diff --git a/package.json b/package.json index 13cd1fe..8b70a72 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@tanstack/react-query-devtools": "^5.71.0", "axios": "^1.8.4", "iron-session": "^8.0.4", + "lucide": "^0.503.0", "mssql": "^11.0.1", "next": "15.2.4", "react": "^19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 920681e..5fbe4ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: iron-session: specifier: ^8.0.4 version: 8.0.4 + lucide: + specifier: ^0.503.0 + version: 0.503.0 mssql: specifier: ^11.0.1 version: 11.0.1 @@ -1100,6 +1103,9 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lucide@0.503.0: + resolution: {integrity: sha512-ZAVlxBU4dbSUAVidb2eT0fH3bTtKCj7M2aZNAVsFOrcnazvYJFu6I8OxFE+Fmx5XNf22Cw4Ln3NBHfBxNfoFOw==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2319,6 +2325,8 @@ snapshots: lodash.once@4.1.1: {} + lucide@0.503.0: {} + math-intrinsics@1.1.0: {} micromatch@4.0.8: diff --git a/src/api/surveySales.ts b/src/api/surveySales.ts index e54f856..57314da 100644 --- a/src/api/surveySales.ts +++ b/src/api/surveySales.ts @@ -2,67 +2,123 @@ import { axiosInstance } from '@/libs/axios' export interface SurveySalesBasicInfo { id?: number - representative: String - store: String | null - construction_point: String | null - investigation_date: String | null - building_name: String | null - customer_name: String | null - post_code: String | null - address: String | null - address_detail: String | null - submission_status: Boolean - submission_date?: String | null + representative: string + store: string | null + construction_point: string | null + investigation_date: string | null + building_name: string | null + customer_name: string | null + post_code: string | null + address: string | null + address_detail: string | null + submission_status: boolean + submission_date?: string | null detail_info?: SurveySalesDetailInfo | null } export interface SurveySalesDetailInfo { id?: number - contract_capacity: String | null - retail_company: String | null - supplementary_facilities: Number | null - supplementary_facilities_etc: String | null - installation_system: Number | null - installation_system_etc: String | null - construction_year: Number | null - construction_year_etc: String | null - roof_material: Number | null - roof_material_etc: String | null - roof_shape: Number | null - roof_shape_etc: String | null - roof_slope: String | null - house_structure: Number | null - house_structure_etc: String | null - rafter_material: Number | null - rafter_material_etc: String | null - rafter_size: Number | null - rafter_size_etc: String | null - rafter_pitch: Number | null - rafter_pitch_etc: String | null - rafter_direction: Number | null - open_field_plate_kind: Number | null - open_field_plate_kind_etc: String | null - open_field_plate_thickness: String | null - leak_trace: Boolean | null - waterproof_material: Number | null - waterproof_material_etc: String | null - insulation_presence: Number | null - insulation_presence_etc: String | null - structure_order: Number | null - structure_order_etc: String | null - installation_availability: Number | null - installation_availability_etc: String | null - memo: String | null + contract_capacity: string | null + retail_company: string | null + supplementary_facilities: number | null + supplementary_facilities_etc: string | null + installation_system: number | null + installation_system_etc: string | null + construction_year: number | null + construction_year_etc: string | null + roof_material: number | null + roof_material_etc: string | null + roof_shape: number | null + roof_shape_etc: string | null + roof_slope: string | null + house_structure: number | null + house_structure_etc: string | null + rafter_material: number | null + rafter_material_etc: string | null + rafter_size: number | null + rafter_size_etc: string | null + rafter_pitch: number | null + rafter_pitch_etc: string | null + rafter_direction: number | null + open_field_plate_kind: number | null + open_field_plate_kind_etc: string | null + open_field_plate_thickness: string | null + leak_trace: boolean | null + waterproof_material: number | null + waterproof_material_etc: string | null + insulation_presence: number | null + insulation_presence_etc: string | null + structure_order: number | null + structure_order_etc: string | null + installation_availability: number | null + installation_availability_etc: string | null + memo: string | null } export const surveySalesApi = { - create: async (data: SurveySalesBasicInfo): Promise => { - const response = await axiosInstance.post('/api/survey-sales', data) - return response.data + create: async (data: SurveySalesBasicInfo): Promise => { + try { + const response = await axiosInstance.post('/api/survey-sales', data) + return response.data.id ?? 0 + } catch (error) { + console.error(error) + return 0 + } }, - getList: async (): Promise => { - const response = await axiosInstance.get('/api/survey-sales') - return response.data + getList: async (): Promise => { + try { + const response = await axiosInstance.get('/api/survey-sales') + return response.data + } catch (error) { + console.error(error) + return [] + } + }, + getDetail: async (id: number): Promise => { + try { + const response = await axiosInstance.get(`/api/survey-sales/${id}`) + return response.data + } catch (error) { + console.error(error) + return null + } + }, + update: async (id: number, data: SurveySalesBasicInfo): Promise => { + try { + const response = await axiosInstance.put(`/api/survey-sales/${id}`, data) + return response.data + } catch (error) { + console.error(error) + return null + } + }, + delete: async (id: number, isDetail: boolean = false): Promise => { + try { + await axiosInstance.delete(`/api/survey-sales/${id}`, { + params: { + detail_id: isDetail ? id : undefined, + }, + }) + return true + } catch (error) { + throw error + } + }, + createDetail: async (surveyId: number, data: SurveySalesDetailInfo): Promise => { + try { + await axiosInstance.post(`/api/survey-sales/${surveyId}`, data) + return true + } catch (error) { + throw error + } + }, + confirm: async (id: number): Promise => { + try { + await axiosInstance.patch(`/api/survey-sales/${id}`) + return true + } catch (error) { + throw error + } }, update: async (data: SurveySalesBasicInfo): Promise => { const response = await axiosInstance.put(`/api/survey-sales`, data) diff --git a/src/app/api/survey-sales/[id]/route.ts b/src/app/api/survey-sales/[id]/route.ts new file mode 100644 index 0000000..bf5afe4 --- /dev/null +++ b/src/app/api/survey-sales/[id]/route.ts @@ -0,0 +1,72 @@ +import { NextResponse } from 'next/server' + +export async function POST(request: Request, context: { params: { id: string } }) { + const body = await request.json() + const { id } = await context.params + + // @ts-ignore + const survey = await prisma.SD_SERVEY_SALES_BASIC_INFO.update({ + where: { id: Number(id) }, + data: { + detail_info: { + create: body, + }, + }, + }) + return NextResponse.json({ message: 'Survey detail created successfully' }) +} +export async function GET(request: Request, context: { params: { id: string } }) { + const { id } = await context.params + // @ts-ignore + const survey = await prisma.SD_SERVEY_SALES_BASIC_INFO.findUnique({ + where: { id: Number(id) }, + include: { + detail_info: true, + }, + }) + return NextResponse.json(survey) +} + +export async function PUT(request: Request, context: { params: { id: string } }) { + const { id } = await context.params + const body = await request.json() + // @ts-ignore + const survey = await prisma.SD_SERVEY_SALES_BASIC_INFO.update({ + where: { id: Number(id) }, + data: { + ...body, + detail_info: { + update: body.detail_info, + }, + }, + }) + return NextResponse.json(survey) +} + +export async function DELETE(request: Request, context: { params: { id: string; detail_id: string } }) { + const { id, detail_id } = await context.params + if (detail_id) { + // @ts-ignore + const survey = await prisma.SD_SERVEY_SALES_DETAIL_INFO.delete({ + where: { id: Number(detail_id) }, + }) + } else { + // @ts-ignore + const survey = await prisma.SD_SERVEY_SALES_BASIC_INFO.delete({ + where: { id: Number(id) }, + }) + } + return NextResponse.json({ message: 'Survey deleted successfully' }) +} + +export async function PATCH(request: Request, context: { params: { id: string } }) { + const { id } = await context.params + // @ts-ignore + const survey = await prisma.SD_SERVEY_SALES_BASIC_INFO.update({ + where: { id: Number(id) }, + data: { + submission_status: true, + }, + }) + return NextResponse.json({ message: 'Survey confirmed successfully' }) +} diff --git a/src/app/api/survey-sales/route.ts b/src/app/api/survey-sales/route.ts index 7662111..b460751 100644 --- a/src/app/api/survey-sales/route.ts +++ b/src/app/api/survey-sales/route.ts @@ -3,13 +3,21 @@ import { prisma } from '@/libs/prisma' export async function POST(request: Request) { const body = await request.json() - // @ts-ignore const res = await prisma.SD_SERVEY_SALES_BASIC_INFO.create({ data: body, }) + return NextResponse.json(res) +} - return NextResponse.json({ message: 'Survey sales created successfully' }) +export async function GET() { + try { + // @ts-ignore + const res = await prisma.SD_SERVEY_SALES_BASIC_INFO.findMany() + return NextResponse.json(res) + } catch (error) { + console.error(error) + } } export async function GET(request: Request) { diff --git a/src/app/inquiry/[id]/page.tsx b/src/app/inquiry/[id]/page.tsx new file mode 100644 index 0000000..60271a3 --- /dev/null +++ b/src/app/inquiry/[id]/page.tsx @@ -0,0 +1,9 @@ +import InquiryDetail from '@/components/inquiry/InquiryDetail' + +export default function InquiryDetails() { + return ( +
+ +
+ ) +} diff --git a/src/app/inquiry/page.tsx b/src/app/inquiry/page.tsx new file mode 100644 index 0000000..0393249 --- /dev/null +++ b/src/app/inquiry/page.tsx @@ -0,0 +1,9 @@ +import InquiryList from '@/components/inquiry/InquiryList' + +export default function Inquiry() { + return ( +
+ +
+ ) +} diff --git a/src/app/inquiry/write/page.tsx b/src/app/inquiry/write/page.tsx new file mode 100644 index 0000000..2d1a122 --- /dev/null +++ b/src/app/inquiry/write/page.tsx @@ -0,0 +1,9 @@ +import InquiryWriteForm from '@/components/inquiry/InquiryWriteForm' + +export default function InquiryWrite() { + return ( +
+ +
+ ) +} diff --git a/src/app/survey-sales/[id]/page.tsx b/src/app/survey-sales/[id]/page.tsx new file mode 100644 index 0000000..eb6685c --- /dev/null +++ b/src/app/survey-sales/[id]/page.tsx @@ -0,0 +1,6 @@ +import SurveyDetail from '@/components/survey-sales/SurveyDetail' + +export default function SurveySalesDetailPage() { + return +} + diff --git a/src/app/survey-sales/page.tsx b/src/app/survey-sales/page.tsx index 6671ce4..b04c6c1 100644 --- a/src/app/survey-sales/page.tsx +++ b/src/app/survey-sales/page.tsx @@ -1,10 +1,12 @@ import SurveySales from '@/components/SurveySales' +import SurveySaleList from '@/components/survey-sales/SurveySaleList' export default function page() { return ( <>

조사 매물 정보

- + {/* */} + ) } diff --git a/src/app/survey-sales/write/page.tsx b/src/app/survey-sales/write/page.tsx new file mode 100644 index 0000000..5d7b38f --- /dev/null +++ b/src/app/survey-sales/write/page.tsx @@ -0,0 +1,9 @@ +import MainSurveyForm from '@/components/survey-sales/write-survey-sales/MainSurveyForm' + +export default function SurveyWritePage() { + return ( +
+ +
+ ) +} diff --git a/src/components/LoadMoreButton.tsx b/src/components/LoadMoreButton.tsx new file mode 100644 index 0000000..0a92e1b --- /dev/null +++ b/src/components/LoadMoreButton.tsx @@ -0,0 +1,11 @@ +'use client' + +interface LoadMoreButtonProps { + hasMore: boolean + onLoadMore: () => void + onScrollToTop: () => void +} + +export default function LoadMoreButton({ hasMore, onLoadMore, onScrollToTop }: LoadMoreButtonProps) { + return
{hasMore ? : }
+} diff --git a/src/components/inquiry/InquiryDetail.tsx b/src/components/inquiry/InquiryDetail.tsx new file mode 100644 index 0000000..b153f35 --- /dev/null +++ b/src/components/inquiry/InquiryDetail.tsx @@ -0,0 +1,73 @@ +'use client' + +import { useParams } from 'next/navigation' + +const inquiryDummyData = { + writer: { + name: 'writer', + email: 'writer@example.com', + }, + title: 'title', + content: 'content', + files: ['file1.jpg', 'file2.jpg', 'file3.jpg'], + createdAt: '2021-01-01', + answer: { + writer: '佐藤一貴', + content: + '一次側接続は、自動切替開閉器と住宅分電盤主幹ブレーカの間に蓄電システムブレーカを配線する方法です。\n二次側接続は、住宅分電盤主幹ブレ―カの二次側に蓄電システムブレーカを接続する', + createdAt: '2021-01-01 12:00:00', + files: ['file4.jpg', 'file5.jpg', 'file6.jpg'], + }, +} + +export default function InquiryDetail() { + const params = useParams() + const id = params.id + return ( +
+

InquiryDetail

+

{id}

+
+
+

writer

+

{inquiryDummyData.writer.name}

+
+
+

email

+

{inquiryDummyData.writer.email}

+
+
+

title

+

{inquiryDummyData.title}

+
+
+

content

+

{inquiryDummyData.content}

+
+
+

files

+
+ {inquiryDummyData.files.map((file) => ( + {file} + ))} +
+
+ {inquiryDummyData.answer && ( +
+

Reply: Hanwha Japan

+
+

{inquiryDummyData.answer.writer}

+

{inquiryDummyData.answer.createdAt}

+

{inquiryDummyData.answer.content}

+
+ {inquiryDummyData.answer.files.map((file) => ( + {file} + ))} +
+
+
+ )} +
+
+ ) +} diff --git a/src/components/inquiry/InquiryFilter.tsx b/src/components/inquiry/InquiryFilter.tsx new file mode 100644 index 0000000..c3911a2 --- /dev/null +++ b/src/components/inquiry/InquiryFilter.tsx @@ -0,0 +1,20 @@ +'use client' + +import { Search } from 'lucide-react' +import { useRouter } from 'next/navigation' + + +export default function InquiryFilter({ handleSearch }: { handleSearch: (e: React.ChangeEvent) => void }) { + const router = useRouter() + return ( +
+ +
+ + +
+
+ ) +} diff --git a/src/components/inquiry/InquiryItems.tsx b/src/components/inquiry/InquiryItems.tsx new file mode 100644 index 0000000..bc38ad6 --- /dev/null +++ b/src/components/inquiry/InquiryItems.tsx @@ -0,0 +1,21 @@ +'use client' + +import { useRouter } from 'next/navigation' + +export default function InquiryItems({ inquiryData }: { inquiryData: any }) { + const router = useRouter() + return ( +
+ {inquiryData.map((item: any) => ( +
router.push(`/inquiry/${item.id}`)}> +
{item.title}
+
{item.content}
+
{item.createdAt}
+
{item.writer}
+
{item.category}
+ {item.file &&
{item.file}
} +
+ ))} +
+ ) +} diff --git a/src/components/inquiry/InquiryList.tsx b/src/components/inquiry/InquiryList.tsx new file mode 100644 index 0000000..f65478b --- /dev/null +++ b/src/components/inquiry/InquiryList.tsx @@ -0,0 +1,171 @@ +'use client' +import { useState } from 'react' +import InquiryItems from './InquiryItems' +import InquiryFilter from './InquiryFilter' +import LoadMoreButton from '../LoadMoreButton' + +const inquiryDummyData = [ + { + id: 1, + title: 'post', + content: 'content', + file: 'file.png', + createdAt: '2024-01-01', + writer: 'writer', + category: 'A', + }, + { + id: 2, + title: 'post', + content: 'content', + file: 'file.png', + createdAt: '2024-01-01', + writer: 'writer1', + category: 'B', + }, + { + id: 3, + title: 'post', + content: 'content', + file: null, + createdAt: '2024-01-01', + writer: 'writer1', + category: 'C', + }, + { + id: 4, + title: 'post', + content: 'content', + file: null, + createdAt: '2024-01-01', + writer: 'writer1', + category: 'A', + }, + { + id: 5, + title: 'post', + content: 'content', + file: null, + createdAt: '2024-01-01', + writer: 'writer1', + category: 'B', + }, + { + id: 6, + title: 'post', + content: 'content', + file: null, + createdAt: '2024-01-01', + writer: 'writer1', + category: 'C', + }, + { + id: 7, + title: 'post', + content: 'content', + file: 'file.png', + createdAt: '2024-01-01', + writer: 'writer', + category: 'A', + }, + { + id: 8, + title: 'post', + content: 'content', + file: 'file.png', + createdAt: '2024-01-01', + writer: 'writer1', + category: 'B', + }, + { + id: 9, + title: 'post', + content: 'content', + file: null, + createdAt: '2024-01-01', + writer: 'writer1', + category: 'C', + }, + + { + id: 10, + title: 'post', + content: 'content', + file: 'file.png', + createdAt: '2024-01-01', + writer: 'writer1', + category: 'A', + }, + { + id: 11, + title: 'post', + content: 'content', + file: 'file.png', + createdAt: '2024-01-01', + writer: 'writer', + category: 'B', + }, + { + id: 12, + title: 'post', + content: 'content', + file: null, + createdAt: '2024-01-01', + writer: 'writer1', + category: 'C', + }, +] + +export default function InquiryList() { + const [visibleItems, setVisibleItems] = useState(5) + const [isMyPostsOnly, setIsMyPostsOnly] = useState(false) + const [category, setCategory] = useState('') + const [search, setSearch] = useState('') + const [hasMore, setHasMore] = useState(inquiryDummyData.length > 5) + + const inquriyData = () => { + if (isMyPostsOnly) { + return inquiryDummyData.filter((item) => item.writer === 'writer') + } + if (category.trim().length > 0) { + return inquiryDummyData.filter((item) => item.category === category) + } + if (search.trim().length > 0) { + return inquiryDummyData.filter((item) => item.title.includes(search)) + } + return inquiryDummyData + } + + const handleLoadMore = () => { + const newVisibleItems = Math.min(visibleItems + 5, inquriyData().length) + setVisibleItems(newVisibleItems) + setHasMore(newVisibleItems < inquriyData().length) + } + + const handleSearch = (e: React.ChangeEvent) => { + setSearch(e.target.value) + } + + const handleScrollToTop = () => { + window.scrollTo({ top: 0, behavior: 'smooth' }) + } + + return ( +
+ +
+ setIsMyPostsOnly(e.target.checked)} /> + +
+ + total {inquriyData().length} + + +
+ ) +} diff --git a/src/components/inquiry/InquiryWriteForm.tsx b/src/components/inquiry/InquiryWriteForm.tsx new file mode 100644 index 0000000..047e4d0 --- /dev/null +++ b/src/components/inquiry/InquiryWriteForm.tsx @@ -0,0 +1,72 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' + +export interface InquiryFormData { + category: string + title: string + content: string + file: File[] +} + +export default function InquiryWriteForm() { + const router = useRouter() + const [formData, setFormData] = useState({ + category: 'A', + title: '', + content: '', + file: [], + }) + + const handleFileChange = (e: React.ChangeEvent) => { + const file = Array.from(e.target.files || []) + setFormData({ ...formData, file: [...formData.file, ...file] }) + } + + const handleFileDelete = (fileToDelete: File) => { + setFormData({ ...formData, file: formData.file.filter((f) => f !== fileToDelete) }) + } + + const handleSubmit = () => { + console.log('submit: ', formData) + // router.push(`/inquiry`) + } + + return ( +
+
+ + +
+
+ + setFormData({ ...formData, title: e.target.value })} /> +
+
+ +