From 69571e86b85add0972ec76e0c8c9a033ccc56490 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Mon, 2 Jun 2025 11:34:25 +0900 Subject: [PATCH 1/5] refactor: update Prisma schema to enhance data models - Added BC_QM_BUILDER, BC_QM_TERMS, BC_QM_USER, IF_PERSON_OFFICE_MAPPING, MS_CUST_AGENCY_STOREID, MS_CUST_H, MS_CUST_PERSON, MS_CUST_STOREID, MS_CUST_STOREID_ADDITNL models to the Prisma schema for improved data structure. - Reintroduced CONSTRUCTION_POINT_ID and SUBMISSION_TARGET_NM fields in SD_SURVEY_SALES_BASIC_INFO model for consistency and enhanced data representation. --- prisma/schema.prisma | 305 +++++++++++++++++++++- src/app/api/submission/admin-sub/route.ts | 41 +++ src/app/api/submission/admin/route.ts | 46 ++++ src/app/api/submission/builder/route.ts | 42 +++ src/app/api/submission/super/route.ts | 39 +++ 5 files changed, 471 insertions(+), 2 deletions(-) create mode 100644 src/app/api/submission/admin-sub/route.ts create mode 100644 src/app/api/submission/admin/route.ts create mode 100644 src/app/api/submission/builder/route.ts create mode 100644 src/app/api/submission/super/route.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 45a6939..778ea0c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,7 +13,6 @@ model SD_SURVEY_SALES_BASIC_INFO { REPRESENTATIVE String @db.NVarChar(200) STORE String? @db.NVarChar(200) CONSTRUCTION_POINT String? @db.NVarChar(200) - CONSTRUCTION_POINT_ID String? @db.NVarChar(200) INVESTIGATION_DATE String? @db.NVarChar(10) BUILDING_NAME String? @db.NVarChar(200) CUSTOMER_NAME String? @db.NVarChar(200) @@ -23,11 +22,12 @@ model SD_SURVEY_SALES_BASIC_INFO { SUBMISSION_STATUS Boolean @default(false) SUBMISSION_DATE DateTime? @db.Date SUBMISSION_TARGET_ID String? @db.NVarChar(200) - SUBMISSION_TARGET_NM String? @db.NVarChar(200) REG_DT DateTime @default(now()) UPT_DT DateTime @updatedAt REPRESENTATIVE_ID String? @db.NVarChar(100) STORE_ID String? @db.NVarChar(100) + CONSTRUCTION_POINT_ID String? @db.NVarChar(200) + SUBMISSION_TARGET_NM String? @db.NVarChar(200) DETAIL_INFO SD_SURVEY_SALES_DETAIL_INFO? } @@ -179,3 +179,304 @@ model MS_USR_TRK { REG_DT DateTime @default(now()) DATA String? @db.NVarChar(200) } + +model BC_QM_BUILDER { + COMP_CD String @db.NVarChar(4) + BUILDER_NO String @db.NVarChar(50) + AGENCY_STORE_ID String @db.NVarChar(100) + BUILDER_NM String @db.NVarChar(100) + BUILDER_ID String? @db.NVarChar(100) + ZIP_NO String? @db.NVarChar(7) + PRFT_JP String? @db.NVarChar(100) + MNCP_NM String? @db.NVarChar(100) + ADDR_NM String? @db.NVarChar(200) + ADDR_DTL String? @db.NVarChar(200) + RECEIVER_NM String? @db.NVarChar(100) + RECEIVER_CONTACT_NO String? @db.NVarChar(100) + USE_YN String? @db.NVarChar(1) + REG_DT DateTime? @db.DateTime + REG_ID String? @db.NVarChar(50) + UPT_DT DateTime? @db.DateTime + UPT_ID String? @db.NVarChar(50) + BC_QM_USER BC_QM_USER[] + + @@id([COMP_CD, BUILDER_NO], map: "PK_BC_QM_BUILDER") +} + +model BC_QM_TERMS { + COMP_CD String @db.NVarChar(4) + TERMS_NO Decimal @db.Decimal(18, 0) + VER_NO String @db.NVarChar(3) + VER_SUB_NO String @db.NVarChar(3) + OPERTN_DT DateTime @db.Date + TERMS_CTNT String @db.NVarChar(Max) + DEL_YN String? @db.NVarChar(1) + REG_DT DateTime? @db.DateTime + REG_ID String? @db.NVarChar(50) + UPT_DT DateTime? @db.DateTime + UPT_ID String? @db.NVarChar(50) + BC_QM_USER BC_QM_USER[] + + @@id([TERMS_NO, COMP_CD], map: "PK_BC_QM_TERMS") +} + +model BC_QM_USER { + COMP_CD String @db.NVarChar(4) + USER_ID String @db.NVarChar(50) + AGENCY_STORE_ID String @db.NVarChar(100) + USER_NM String @db.NVarChar(100) + USER_AUTH_CD String @db.NVarChar(10) + EMAIL String? @db.NVarChar(100) + PSTN_NM String? @db.NVarChar(100) + TEL_NO String? @db.NVarChar(100) + MOBILE_NO String? @db.NVarChar(100) + FAX_NO String? @db.NVarChar(100) + LAST_LOGIN_DT DateTime? @db.DateTime + PWD String @db.NVarChar(100) + PWD_INIT_YN String? @default("N", map: "DF__BC_QM_USE__PWD_I__42E1EEFE") @db.NVarChar(1) + LOGIN_FAIL_CNT Decimal? @default(0, map: "DF__BC_QM_USE__LOGIN__43D61337") @db.Decimal(18, 0) + LAST_LOGIN_FAIL_DT DateTime? @db.DateTime + LAST_PWD_UPT_DT DateTime? @db.DateTime + BUILDER_NO String? @db.NVarChar(50) + ZIP_NO String? @db.NVarChar(7) + PRFT_JP String? @db.NVarChar(100) + MNCP_NM String? @db.NVarChar(100) + ADDR_NM String? @db.NVarChar(200) + TERMS_AGREE_YN String? @default("N", map: "DF__BC_QM_USE__TERMS__44CA3770") @db.NVarChar(1) + TERMS_AGREE_DT DateTime? @db.DateTime + AGREE_TERMS_NO Decimal? @db.Decimal(18, 0) + STAT_CD String? @db.NVarChar(1) + DEL_YN String? @db.NVarChar(1) + REG_DT DateTime? @db.DateTime + REG_ID String? @db.NVarChar(50) + UPT_DT DateTime? @db.DateTime + UPT_ID String? @db.NVarChar(50) + USER_NM_KANA String? @db.NVarChar(50) + BC_QM_BUILDER BC_QM_BUILDER? @relation(fields: [COMP_CD, BUILDER_NO], references: [COMP_CD, BUILDER_NO], onUpdate: NoAction, map: "FK_BC_QM_USER_01") + BC_QM_TERMS BC_QM_TERMS? @relation(fields: [AGREE_TERMS_NO, COMP_CD], references: [TERMS_NO, COMP_CD], onUpdate: NoAction, map: "FK_BC_QM_USER_02") + + @@id([COMP_CD, USER_ID], map: "PK_BC_QM_USER") +} + +model IF_PERSON_OFFICE_MAPPING { + IF_SEQ Decimal @id(map: "PK_IF_PERSON_OFFICE_MAPPING") @db.Decimal(22, 0) + SEQ Int? + LIFNR String? @db.NVarChar(10) + VKBUR String? @db.NVarChar(4) + IF_DT DateTime? @db.DateTime + IF_STS String? @db.NVarChar(1) + IF_MSG String? @db.NVarChar(200) +} + +model MS_CUST_AGENCY_STOREID { + COMP_CD String @db.NVarChar(4) + STORE_ID String @db.NVarChar(100) + AGENCY_STORE_ID String @db.NVarChar(100) + AGENCY_QCAST_NM String? @db.NVarChar(100) + PLAN_REQ_SUBMIT_YN String? @default("N", map: "DF__MS_CUST_A__PLAN___3B40CD36") @db.NVarChar(1) + DEL_YN String? @default("N", map: "DF__MS_CUST_A__DEL_Y__3C34F16F") @db.NVarChar(1) + REG_DT DateTime? @db.DateTime + REG_ID String? @db.NVarChar(50) + UPT_DT DateTime? @db.DateTime + UPT_ID String? @db.NVarChar(50) + EMAIL String? @db.NVarChar(100) + REMARK String? @db.NVarChar(Max) + BDL_TP_CD String? @db.NVarChar(10) + + @@id([COMP_CD, AGENCY_STORE_ID], map: "PK_MS_CUST_AGENCY_STOREID") +} + +model MS_CUST_H { + COMP_CD String @db.NVarChar(4) + CUST_CD String @db.NVarChar(10) + ACCOUNT_GR String? @db.NVarChar(4) + CUST_NM String? @db.NVarChar(70) + HAUS_NO String? @db.NVarChar(10) + POST_CD String? @db.NVarChar(10) + CITY_CD String? @db.NVarChar(12) + CITY_NM String? @db.NVarChar(40) + COUNTRY_CD String? @db.NVarChar(4) + REGION_CD String? @db.NVarChar(4) + STREET_NM String? @db.NVarChar(60) + STREET_NM2 String? @db.NVarChar(60) + STREET_NM3 String? @db.NVarChar(60) + STREET_NM4 String? @db.NVarChar(60) + STREET_NM5 String? @db.NVarChar(60) + TIME_ZONE String? @db.NVarChar(10) + TRANSP_ZONE String? @db.NVarChar(10) + LANG_CD String? @db.NVarChar(2) + TEL_NO String? @db.NVarChar(30) + TEL_NO_EXT String? @db.NVarChar(10) + MOBILE_NO String? @db.NVarChar(30) + FAX_NO String? @db.NVarChar(30) + FAX_NO_EXT String? @db.NVarChar(10) + EMAIL String? @db.NVarChar(100) + INDUSTRY_CD String? @db.NVarChar(4) + VAT_REG_NO String? @db.NVarChar(20) + LOCAT_NO1 Decimal? @db.Decimal(7, 0) + LOCAT_NO2 Decimal? @db.Decimal(5, 0) + RESI_CD String? @db.NVarChar(4) + UTIL_CD String? @db.NVarChar(4) + MARK_PF String? @db.NVarChar(4) + INVS_CD String? @db.NVarChar(4) + PRMT_CD String? @db.NVarChar(4) + SVC_PR_CD String? @db.NVarChar(4) + ENG_CNSL_CD String? @db.NVarChar(4) + RECON_ACC_CD String? @db.NVarChar(20) + SORT_KEY String? @db.NVarChar(12) + CASH_MGMT_GR String? @db.NVarChar(12) + INTEREST_IND_CD String? @db.NVarChar(12) + TERM_PAY_CD String? @db.NVarChar(12) + PAY_HIST_YN String? @db.NVarChar(2) + DUN_PROC_CD String? @db.NVarChar(4) + DUN_BLOCK_CD String? @db.NVarChar(1) + DUN_LEVEL_CD Decimal? @db.Decimal(1, 0) + POLICY_NO String? @db.NVarChar(20) + INSURED_AMT Decimal? @db.Decimal(13, 3) + VAL_TO_DATE DateTime? @db.Date + DEDUCT_RATE Decimal? @db.Decimal(9, 0) + APPLIED_INSURA Decimal? @db.Decimal(13, 3) + EASY_NO String? @db.NVarChar(20) + APPLY_DATE DateTime? @db.Date + EXPIRE_DATE DateTime? @db.Date + ADD_TXT1 String? @db.NVarChar(400) + ADD_TXT2 String? @db.NVarChar(400) + REF_PS_ID String? @db.NVarChar(20) + CUST_COMP_CD String? @db.NVarChar(4) + CUST_TEXT String? @db.NVarChar(Max) + CUST_GRADE String? @db.NVarChar(2) + INCOME_TAX_NO String? @db.NVarChar(20) + STAT_CD String? @db.NVarChar(2) + REG_DT DateTime? @db.DateTime + REG_ID String? @db.NVarChar(50) + UPT_DT DateTime? @db.DateTime + UPT_ID String? @db.NVarChar(50) + QSP_ACCOUNT_GR String? @db.NVarChar(4) + BDL_TP_CD String? @db.NVarChar(10) + BDL_SUB_TP_CD String? @db.NVarChar(100) + MS_CUST_STOREID MS_CUST_STOREID[] + + @@id([COMP_CD, CUST_CD], map: "PK_MS_CUST_H") +} + +model MS_CUST_PERSON { + SEQ Decimal @db.Decimal(22, 0) + COMP_CD String @db.NVarChar(4) + CUST_CD String @db.NVarChar(10) + EOS_LOGIN_ID String? @db.NVarChar(100) + EOS_PWD String? @db.NVarChar(100) + NAME String? @db.NVarChar(50) + PSTN_NM String? @db.NVarChar(50) + EMAIL String? @db.NVarChar(100) + TEL_NO String? @db.NVarChar(100) + AUTHORITY String @db.NVarChar(1) + DEL_YN String? @default("N", map: "DF__MS_CUST_P__DEL_Y__339FAB6E") @db.NVarChar(1) + LOGIN_DT DateTime? @db.DateTime + PWD_INIT_YN String? @db.NVarChar(1) + LOGIN_FAIL_CNT Decimal? @db.Decimal(18, 0) + REG_DT DateTime? @db.DateTime + REG_ID String? @db.NVarChar(50) + UPT_DT DateTime? @db.DateTime + UPT_ID String? @db.NVarChar(50) + LOGIN_FAIL_DT DateTime? @db.DateTime + PLAN_REQ_AUTH String? @default("N", map: "DF__MS_CUST_P__PLAN___3493CFA7") @db.NVarChar(1) + PRICE_VIEW_STAT_CD String? @default("S", map: "DF__MS_CUST_P__PRICE__3587F3E0") @db.NVarChar(10) + SD_SUBMIT_YN String? @default("N", map: "DF__MS_CUST_P__SD_SU__367C1819") @db.NVarChar(1) + STORE_ID String? @db.NVarChar(100) + USER_NM_KANA String? @db.NVarChar(50) + FAX String? @db.NVarChar(100) + PLAN_MAIL_RCV_YN String? @default("Y", map: "DF__MS_CUST_P__PLAN___37703C52") @db.NVarChar(1) + SD_MAIL_RCV_YN String? @default("Y", map: "DF__MS_CUST_P__SD_MA__3864608B") @db.NVarChar(1) + + @@id([SEQ, COMP_CD, CUST_CD], map: "PK_MS_CUST_PERSON") +} + +model MS_CUST_STOREID { + COMP_CD String @db.NVarChar(4) + STORE_ID String @db.NVarChar(100) + CUST_CD String @db.NVarChar(10) + REPRESENTATIVE_STORE_YN String? @db.NVarChar(1) + SP_MODULE_PRICE_SEQ Decimal? @db.Decimal(20, 0) + SP_MODULE_PRICE_EXP_FR_DT DateTime? @db.Date + SP_MODULE_PRICE_EXP_TO_DT DateTime? @db.Date + SP_BOS_PRICE_SEQ Decimal? @db.Decimal(20, 0) + SP_BOS_PRICE_EXP_FR_DT DateTime? @db.Date + SP_BOS_PRICE_EXP_TO_DT DateTime? @db.Date + PKG_PRICE_RANK String? @default("C", map: "DF__MS_CUST_S__PKG_P__29221CFB") @db.NVarChar(3) + SP_PKG_PRICE Decimal? @db.Decimal(13, 1) + SP_PKG_PRICE_EXP_FR_DT DateTime? @db.Date + SP_PKG_PRICE_EXP_TO_DT DateTime? @db.Date + DEL_YN String? @default("N", map: "DF__MS_CUST_S__DEL_Y__2A164134") @db.NVarChar(1) + REG_DT DateTime? @db.DateTime + REG_ID String? @db.NVarChar(50) + UPT_DT DateTime? @db.DateTime + UPT_ID String? @db.NVarChar(50) + MOVE_DT DateTime? @db.DateTime + MOVE_ID String? @db.NVarChar(50) + STORE_QCAST_NM String? @db.NVarChar(100) + PLAN_REQ_AUTH String? @default("N", map: "DF__MS_CUST_S__PLAN___2B0A656D") @db.NVarChar(1) + PKG_EXCEPT_YN String? @default("N", map: "DF__MS_CUST_S__PKG_E__2BFE89A6") @db.NVarChar(1) + MODULE_PRICE_RANK String? @default("C", map: "DF__MS_CUST_S__MODUL__2CF2ADDF") @db.NVarChar(20) + BOS_PRICE_RANK String? @default("A", map: "DF__MS_CUST_S__BOS_P__2DE6D218") @db.NVarChar(20) + PLAN_REQ_SUBMIT_YN String? @default("N", map: "DF__MS_CUST_S__PLAN___2EDAF651") @db.NVarChar(1) + QSP_SUBAL_YN String? @default("N", map: "DF__MS_CUST_S__QSP_S__2FCF1A8A") @db.NVarChar(1) + MS_CUST_H MS_CUST_H @relation(fields: [COMP_CD, CUST_CD], references: [COMP_CD, CUST_CD], onUpdate: NoAction, map: "MS_CUST_STOREID_FK") + + @@id([COMP_CD, STORE_ID], map: "PK_MS_CUST_STOREID") + @@index([COMP_CD, CUST_CD], map: "IDX_MS_CUST_STOREID_COMP_CD_01") +} + +model MS_CUST_STOREID_ADDITNL { + COMP_CD String @db.NVarChar(20) + STORE_ID String @db.NVarChar(100) + REQ_CUST_CD String? @db.NVarChar(10) + REQ_STORE_QCAST_NM String? @db.NVarChar(100) + STORE_QCAST_NM_KANA String? @db.NVarChar(100) + BIZ_NO String? @db.NVarChar(20) + POST_CD String? @db.NVarChar(10) + ADDR String? @db.NVarChar(255) + TEL_NO String? @db.NVarChar(100) + FAX String? @db.NVarChar(100) + APPR_DT DateTime? @db.DateTime + APPR_ID String? @db.NVarChar(50) + APPR_STAT_CD String? @db.NVarChar(10) + APPR_REMARKS String? @db.NVarChar(300) + REQ_APPR_DT DateTime? @db.DateTime + PAY_TERMS_CD String? @db.NVarChar(10) + FIRST_STORE_ID String? @db.NVarChar(100) + PARENT_STORE_ID String? @db.NVarChar(100) + STORE_LVL Int? + KAM_ID String? @db.NVarChar(50) + QT_COMP_NM String? @db.NVarChar(100) + QT_POST_CD String? @db.NVarChar(10) + QT_ADDR String? @db.NVarChar(255) + QT_TEL_NO String? @db.NVarChar(100) + QT_FAX String? @db.NVarChar(100) + QT_E_MAIL String? @db.NVarChar(100) + ORD_DELI_TARGET String? @db.NVarChar(100) + ORD_DELI_COMP_NM String? @db.NVarChar(100) + ORD_DELI_COMP_USER_NM String? @db.NVarChar(40) + ORD_DELI_TEL_NO String? @db.NVarChar(100) + ORD_DELI_POST_CD String? @db.NVarChar(10) + ORD_DELI_REMARKS String? @db.NVarChar(200) + GUAR_STORE_NM String? @db.NVarChar(100) + GUAR_STORE_POST_CD String? @db.NVarChar(10) + GUAR_STORE_ADDR String? @db.NVarChar(255) + GUAR_TEL_NO String? @db.NVarChar(100) + NORTH_MODULE_YN String? @default("N", map: "DF__MS_CUST_S__NORTH__17036CC0") @db.NVarChar(1) + DEL_YN String? @default("N", map: "DF__MS_CUST_S__DEL_Y__17F790F9") @db.NVarChar(1) + REG_DT DateTime? @db.DateTime + REG_ID String? @db.NVarChar(50) + UPT_DT DateTime? @db.DateTime + UPT_ID String? @db.NVarChar(50) + REQ_MODULE_PRICE_RANK String? @db.NVarChar(20) + REQ_BOS_PRICE_RANK String? @db.NVarChar(20) + REQ_PKG_PRICE_RANK String? @db.NVarChar(3) + REQ_BDL_TP_CD String? @db.NVarChar(10) + REQ_BDL_SUB_TP_CD String? @db.NVarChar(100) + REMARK String? @db.NVarChar(500) + NORTH_MODULE_UPT_DT DateTime? @db.DateTime + NORTH_MODULE_UPT_ID String? @db.NVarChar(50) + + @@id([COMP_CD, STORE_ID], map: "PK_MS_CUST_STOREID_ADDITNL") +} diff --git a/src/app/api/submission/admin-sub/route.ts b/src/app/api/submission/admin-sub/route.ts new file mode 100644 index 0000000..4309e22 --- /dev/null +++ b/src/app/api/submission/admin-sub/route.ts @@ -0,0 +1,41 @@ +import { prisma } from '@/libs/prisma' +import { NextRequest, NextResponse } from 'next/server' + +type AdminSubPerson = { + storeId: string + userId: string + eMail: string + authority: string +} + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const id = searchParams.get('id') + + const query = ` + OPEN SYMMETRIC KEY SYMMETRICKEY DECRYPTION BY CERTIFICATE CERTI_QSPJP; + SELECT + MCS.STORE_ID + , MCP.EOS_LOGIN_ID AS USER_ID + , CONVERT(NVARCHAR(100), DecryptByKey(MCP.EMAIL)) AS E_MAIL + , MCP.AUTHORITY + FROM MS_CUST_STOREID MCS WITH(NOLOCK) + LEFT OUTER JOIN MS_CUST_PERSON MCP WITH(NOLOCK) + ON MCS.COMP_CD = MCP.COMP_CD + AND MCS.STORE_ID = MCP.STORE_ID + AND MCP.DEL_YN = 'N' + WHERE MCS.COMP_CD = '5200' + AND MCS.STORE_ID = (SELECT STORE_ID FROM MS_CUST_AGENCY_STOREID WHERE AGENCY_STORE_ID = '201T01' AND DEL_YN = 'N') + AND MCP.EMAIL IS NOT NULL + AND MCS.DEL_YN = 'N'; + CLOSE SYMMETRIC KEY SYMMETRICKEY; + ` + const suitable: AdminSubPerson[] = await prisma.$queryRawUnsafe(query) + + return NextResponse.json({ message: 'Hello, world!' }) + } catch (error) { + console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) + return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) + } +} diff --git a/src/app/api/submission/admin/route.ts b/src/app/api/submission/admin/route.ts new file mode 100644 index 0000000..0b52031 --- /dev/null +++ b/src/app/api/submission/admin/route.ts @@ -0,0 +1,46 @@ +import { prisma } from '@/libs/prisma' +import { NextRequest, NextResponse } from 'next/server' + +type SuperPerson = { + storeId: string + salesOfficeCd: string + fromEmail: string + toEmail: string +} + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const id = searchParams.get('id') + + const query = ` + OPEN SYMMETRIC KEY SYMMETRICKEY DECRYPTION BY CERTIFICATE CERTI_QSPJP; + SELECT + MCSA.STORE_ID + , BCL.CODE AS SALES_OFFICE_CD + , REF_CHR1 AS FROM_E_MAIL + , REF_CHR2 AS TO_E_MAIL + FROM MS_CUST_STOREID_ADDITNL MCSA WITH(NOLOCK) + LEFT OUTER JOIN IF_PERSON_OFFICE_MAPPING IPOM WITH(NOLOCK) + ON MCSA.KAM_ID = IPOM.LIFNR + AND IF_STS = 'R' + AND VKBUR IS NOT NULL + AND VKBUR != '' + INNER JOIN BC_COMM_L BCL WITH(NOLOCK) + ON BCL.CODE = IPOM.VKBUR + AND BCL.HEAD_CD = '103200' + AND BCL.DEL_YN = 'N' + WHERE MCSA.COMP_CD = '5200' + AND MCSA.STORE_ID = 'A03' + AND MCSA.DEL_YN = 'N' + ; + CLOSE SYMMETRIC KEY SYMMETRICKEY; + ` + const suitable: SuperPerson[] = await prisma.$queryRawUnsafe(query) + + return NextResponse.json({ message: 'Hello, world!' }) + } catch (error) { + console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) + return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) + } +} diff --git a/src/app/api/submission/builder/route.ts b/src/app/api/submission/builder/route.ts new file mode 100644 index 0000000..9e1c5cc --- /dev/null +++ b/src/app/api/submission/builder/route.ts @@ -0,0 +1,42 @@ +import { prisma } from '@/libs/prisma' +import { NextRequest, NextResponse } from 'next/server' + +type BuilderPerson = { + agencyStoreId: string + userId: string + eMail: string + userAuthCd: string +} + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const id = searchParams.get('id') + + const query = ` + OPEN SYMMETRIC KEY SYMMETRICKEY DECRYPTION BY CERTIFICATE CERTI_QSPJP; + SELECT + MCAS.AGENCY_STORE_ID + , BQU.USER_ID + , CONVERT(NVARCHAR(100), DecryptByKey(BQU.EMAIL)) AS E_MAIL + , BQU.USER_AUTH_CD + FROM MS_CUST_AGENCY_STOREID MCAS WITH(NOLOCK) + LEFT OUTER JOIN BC_QM_USER BQU WITH(NOLOCK) + ON MCAS.COMP_CD = BQU.COMP_CD + AND MCAS.AGENCY_STORE_ID = BQU.AGENCY_STORE_ID + AND MCAS.DEL_YN = 'N' + WHERE MCAS.COMP_CD = '5200' + AND MCAS.AGENCY_STORE_ID = '201T01' + AND BQU.EMAIL IS NOT NULL + AND BQU.USER_AUTH_CD != 'B' + AND MCAS.DEL_YN = 'N'; + CLOSE SYMMETRIC KEY SYMMETRICKEY; + ` + const suitable: BuilderPerson[] = await prisma.$queryRawUnsafe(query) + + return NextResponse.json({ message: 'Hello, world!' }) + } catch (error) { + console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) + return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) + } +} diff --git a/src/app/api/submission/super/route.ts b/src/app/api/submission/super/route.ts new file mode 100644 index 0000000..6eed5c7 --- /dev/null +++ b/src/app/api/submission/super/route.ts @@ -0,0 +1,39 @@ +import { prisma } from '@/libs/prisma' +import { NextRequest, NextResponse } from 'next/server' + +type SuperPerson = { + storeId: string + userId: string + eMail: string +} + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const id = searchParams.get('id') + + const query = ` + OPEN SYMMETRIC KEY SYMMETRICKEY DECRYPTION BY CERTIFICATE CERTI_QSPJP; + SELECT + MCSA.STORE_ID + , BU.USER_ID + , CONVERT(NVARCHAR(100), DecryptByKey(BU.E_MAIL)) AS E_MAIL + FROM MS_CUST_STOREID_ADDITNL MCSA WITH(NOLOCK) + LEFT OUTER JOIN BC_USER bu WITH(NOLOCK) + ON MCSA.COMP_CD = BU.COMP_CD + AND MCSA.KAM_ID = BU.KAM_ID + AND BU.STAT_CD = 'A' + AND BU.DEL_YN = 'N' + WHERE MCSA.COMP_CD = '5200' + AND MCSA.STORE_ID = 'A03' + AND MCSA.DEL_YN = 'N'; + CLOSE SYMMETRIC KEY SYMMETRICKEY; + ` + const suitable: SuperPerson[] = await prisma.$queryRawUnsafe(query) + + return NextResponse.json({ message: 'Hello, world!' }) + } catch (error) { + console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) + return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) + } +} From f5022ab4239abe438ec0866b438988af17e4acc0 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Thu, 5 Jun 2025 18:06:04 +0900 Subject: [PATCH 2/5] feat: get submit target store, representative user emails --- .env.development | 3 +- .env.localhost | 3 +- src/app/api/submission/admin-sub/route.ts | 13 ++++---- src/app/api/submission/builder/route.ts | 20 +++++++---- .../popup/SurveySaleSubmitPopup.tsx | 22 ++++++++++--- src/hooks/useSurvey.ts | 33 +++++++++++++++++-- src/types/Survey.ts | 8 +++++ 7 files changed, 81 insertions(+), 21 deletions(-) diff --git a/.env.development b/.env.development index 919d6f9..86dd24a 100644 --- a/.env.development +++ b/.env.development @@ -9,7 +9,8 @@ NEXT_PUBLIC_QSP_API_URL=http://121.168.9.37:8080 # NEXT_PUBLIC_QSP_API_URL=https://jp-dev.qsalesplatform.com #1:1문의 api -NEXT_PUBLIC_INQUIRY_API_URL=https://jp-dev.qsalesplatform.com +# NEXT_PUBLIC_INQUIRY_API_URL=https://jp-dev.qsalesplatform.com +NEXT_PUBLIC_INQUIRY_API_URL=http://1.248.227.176:8120 EMAIL_TITLE_PREFIX=(System Test) diff --git a/.env.localhost b/.env.localhost index eb993b5..089e14e 100644 --- a/.env.localhost +++ b/.env.localhost @@ -9,7 +9,8 @@ NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 # NEXT_PUBLIC_QSP_API_URL=https://jp-dev.qsalesplatform.com #1:1문의 api -NEXT_PUBLIC_INQUIRY_API_URL=https://jp-dev.qsalesplatform.com +# NEXT_PUBLIC_INQUIRY_API_URL=https://jp-dev.qsalesplatform.com +NEXT_PUBLIC_INQUIRY_API_URL=http://1.248.227.176:8120 EMAIL_TITLE_PREFIX= diff --git a/src/app/api/submission/admin-sub/route.ts b/src/app/api/submission/admin-sub/route.ts index 4309e22..c8c30f3 100644 --- a/src/app/api/submission/admin-sub/route.ts +++ b/src/app/api/submission/admin-sub/route.ts @@ -7,7 +7,7 @@ type AdminSubPerson = { eMail: string authority: string } - +// 2차점이 자신에게 매핑 된 1차 판매점과 관리자 정보 조회 export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url) @@ -16,17 +16,18 @@ export async function GET(request: NextRequest) { const query = ` OPEN SYMMETRIC KEY SYMMETRICKEY DECRYPTION BY CERTIFICATE CERTI_QSPJP; SELECT - MCS.STORE_ID - , MCP.EOS_LOGIN_ID AS USER_ID - , CONVERT(NVARCHAR(100), DecryptByKey(MCP.EMAIL)) AS E_MAIL - , MCP.AUTHORITY + MCS.STORE_ID AS targetStoreId + , MCS.STORE_QCAST_NM AS targetStoreNm + , MCP.EOS_LOGIN_ID AS repUserId + , CONVERT(NVARCHAR(100), DecryptByKey(MCP.EMAIL)) AS repUserEmail + , MCP.AUTHORITY AS auth FROM MS_CUST_STOREID MCS WITH(NOLOCK) LEFT OUTER JOIN MS_CUST_PERSON MCP WITH(NOLOCK) ON MCS.COMP_CD = MCP.COMP_CD AND MCS.STORE_ID = MCP.STORE_ID AND MCP.DEL_YN = 'N' WHERE MCS.COMP_CD = '5200' - AND MCS.STORE_ID = (SELECT STORE_ID FROM MS_CUST_AGENCY_STOREID WHERE AGENCY_STORE_ID = '201T01' AND DEL_YN = 'N') + AND MCS.STORE_ID = (SELECT STORE_ID FROM MS_CUST_AGENCY_STOREID WHERE AGENCY_STORE_ID = '${id}' AND DEL_YN = 'N') AND MCP.EMAIL IS NOT NULL AND MCS.DEL_YN = 'N'; CLOSE SYMMETRIC KEY SYMMETRICKEY; diff --git a/src/app/api/submission/builder/route.ts b/src/app/api/submission/builder/route.ts index 9e1c5cc..41c66ce 100644 --- a/src/app/api/submission/builder/route.ts +++ b/src/app/api/submission/builder/route.ts @@ -1,4 +1,5 @@ import { prisma } from '@/libs/prisma' +import { SubmitTargetResponse } from '@/types/Survey' import { NextRequest, NextResponse } from 'next/server' type BuilderPerson = { @@ -8,6 +9,8 @@ type BuilderPerson = { userAuthCd: string } +// 2차점의 시공권한 user가 해당 판매점의 관리자 정보 조회 +// N == 일반유저, S == 수퍼유저, B == 시공권한유저 export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url) @@ -16,25 +19,28 @@ export async function GET(request: NextRequest) { const query = ` OPEN SYMMETRIC KEY SYMMETRICKEY DECRYPTION BY CERTIFICATE CERTI_QSPJP; SELECT - MCAS.AGENCY_STORE_ID - , BQU.USER_ID - , CONVERT(NVARCHAR(100), DecryptByKey(BQU.EMAIL)) AS E_MAIL - , BQU.USER_AUTH_CD + MCAS.AGENCY_STORE_ID AS targetStoreId + , MCAS.AGENCY_QCAST_NM AS targetStoreNm + , BQU.USER_ID AS repUserId + , CONVERT(NVARCHAR(100), DecryptByKey(BQU.EMAIL)) AS repUserEmail + , BQU.USER_AUTH_CD AS auth FROM MS_CUST_AGENCY_STOREID MCAS WITH(NOLOCK) LEFT OUTER JOIN BC_QM_USER BQU WITH(NOLOCK) ON MCAS.COMP_CD = BQU.COMP_CD AND MCAS.AGENCY_STORE_ID = BQU.AGENCY_STORE_ID AND MCAS.DEL_YN = 'N' WHERE MCAS.COMP_CD = '5200' - AND MCAS.AGENCY_STORE_ID = '201T01' + AND MCAS.AGENCY_STORE_ID = '${id}' AND BQU.EMAIL IS NOT NULL AND BQU.USER_AUTH_CD != 'B' AND MCAS.DEL_YN = 'N'; CLOSE SYMMETRIC KEY SYMMETRICKEY; ` - const suitable: BuilderPerson[] = await prisma.$queryRawUnsafe(query) + // const suitable: BuilderPerson[] = await prisma.$queryRawUnsafe(query) - return NextResponse.json({ message: 'Hello, world!' }) + // return NextResponse.json({ message: 'Hello, world!' }) + const data: SubmitTargetResponse[] = await prisma.$queryRawUnsafe(query) + return NextResponse.json(data) } catch (error) { console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) diff --git a/src/components/popup/SurveySaleSubmitPopup.tsx b/src/components/popup/SurveySaleSubmitPopup.tsx index e064628..ce6be9e 100644 --- a/src/components/popup/SurveySaleSubmitPopup.tsx +++ b/src/components/popup/SurveySaleSubmitPopup.tsx @@ -15,7 +15,7 @@ interface SubmitFormData { targetNm: string | null sender: string receiver: string[] | string - reference: string | null + reference: string[] | null title: string contents: string | null } @@ -34,7 +34,7 @@ export default function SurveySaleSubmitPopup() { const { setIsShow } = useSpinnerStore() const { getCommCode } = useCommCode() - const { surveyDetail } = useSurvey(Number(routeId)) + const { surveyDetail, getSubmitTarget } = useSurvey(Number(routeId)) const [submitData, setSubmitData] = useState({ saleBase: null, @@ -54,11 +54,25 @@ export default function SurveySaleSubmitPopup() { getCommCode('SALES_OFFICE_CD').then((codes) => { setCommCodeList(codes) }) + } else if (session?.role === 'Builder' || session?.role === 'Admin_Sub') { + getSubmitTarget({ storeId: surveyDetail?.storeId ?? '', role: session?.role ?? '' }).then((data) => { + if (data) { + setSubmitData({ + ...submitData, + targetId: data[0].targetStoreId, + targetNm: data[0].targetStoreNm, + }) + data.length > 1 && + setSubmitData({ + ...submitData, + receiver: data.filter((item) => item.auth === 'S').map((item) => item.repUserEmail), + reference: data.filter((item) => item.auth === 'N').map((item) => item.repUserEmail), + }) + } + }) } setSubmitData({ ...submitData, - targetId: session?.role === 'Builder' ? surveyDetail?.storeId ?? null : null, - targetNm: session?.role === 'Builder' ? surveyDetail?.store ?? null : null, sender: session?.email ?? '', title: '[HANASYS現地調査] 調査物件が提出. (' + surveyDetail?.srlNo + ')', }) diff --git a/src/hooks/useSurvey.ts b/src/hooks/useSurvey.ts index 20f304c..1fc4994 100644 --- a/src/hooks/useSurvey.ts +++ b/src/hooks/useSurvey.ts @@ -1,5 +1,5 @@ -import type { SurveyBasicInfo, SurveyDetailRequest, SurveyRegistRequest } from '@/types/Survey' -import { useMemo, useEffect } from 'react' +import type { SubmitTargetResponse, SurveyBasicInfo, SurveyDetailRequest, SurveyRegistRequest } from '@/types/Survey' +import { useMemo } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useSurveyFilterStore } from '@/store/surveyFilterStore' import { useSessionStore } from '@/store/session' @@ -71,6 +71,7 @@ export function useSurvey(id?: number): { validateSurveyDetail: (surveyDetail: SurveyDetailRequest) => string getZipCode: (zipCode: string) => Promise refetchSurveyList: () => void + getSubmitTarget: (params: { storeId: string; role: string }) => Promise } { const queryClient = useQueryClient() const { keyword, searchOption, isMySurvey, sort, offset } = useSurveyFilterStore() @@ -278,6 +279,33 @@ export function useSurvey(id?: number): { } } + const getSubmitTarget = async (params: { storeId: string; role: string }): Promise => { + try { + if (!params.storeId) { + alert('販売店IDがありません。') + return null + } + + const endpoints = { + Admin_Sub: `/api/submission/admin-sub?id=${params.storeId}`, + Builder: `/api/submission/builder?id=${params.storeId}`, + } as const + + const endpoint = endpoints[params.role as keyof typeof endpoints] + if (!endpoint) { + alert('権限が間違っています。') + return null + } + + const { data } = await axiosInstance(null).get(endpoint) + return data + } catch (error: any) { + console.error('Failed to fetch submit target:', error) + alert(error.response?.data.error || 'データの取得に失敗しました。') + return null + } + } + return { surveyList: surveyData.data, surveyDetail: surveyDetail as SurveyBasicInfo | null, @@ -293,6 +321,7 @@ export function useSurvey(id?: number): { submitSurvey, validateSurveyDetail, getZipCode, + getSubmitTarget, refetchSurveyList, } } diff --git a/src/types/Survey.ts b/src/types/Survey.ts index 2db1034..7128926 100644 --- a/src/types/Survey.ts +++ b/src/types/Survey.ts @@ -146,3 +146,11 @@ export type SurveyRegistRequest = { } export type Mode = 'CREATE' | 'EDIT' | 'READ' | 'TEMP' // 등록 | 수정 | 상세 | 임시저장 + +export type SubmitTargetResponse = { + targetStoreId: string + targetStoreNm: string + repUserId: string + repUserEmail: string + auth: string +} From 763c9d37cf8b7ccec2a2bb8c17b76481c6b86e26 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Mon, 9 Jun 2025 10:37:48 +0900 Subject: [PATCH 3/5] fix: fix data not loading error when navigating PDF pages without not login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로그인 하지 않고 pdf 링크로 이동 시 데이터 로드 되지 않는 문제 해결 - 로그인 하지 않았을 때 pdf 다운로드 이후 메인 페이지로 이동하도록 수정 --- src/app/api/survey-sales/[id]/route.ts | 4 ++- src/components/pdf/SurveySaleDownloadPdf.tsx | 29 ++++++++++++++----- .../survey-sale/detail/DataTable.tsx | 3 -- .../survey-sale/detail/DetailForm.tsx | 10 ++----- src/components/survey-sale/list/ListTable.tsx | 9 ++---- src/hooks/useSurvey.ts | 16 ++++++---- 6 files changed, 41 insertions(+), 30 deletions(-) diff --git a/src/app/api/survey-sales/[id]/route.ts b/src/app/api/survey-sales/[id]/route.ts index d70471a..8d7806c 100644 --- a/src/app/api/survey-sales/[id]/route.ts +++ b/src/app/api/survey-sales/[id]/route.ts @@ -15,6 +15,7 @@ interface SessionParams { storeId: string | null builderId: string | null isLoggedIn: string | null + isPdf: boolean | null } const checkT01Role = (survey: Survey): boolean => survey.SRL_NO !== '一時保存' @@ -74,6 +75,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ storeId: searchParams.get('storeId'), builderId: searchParams.get('builderId'), isLoggedIn: searchParams.get('isLoggedIn'), + isPdf: searchParams.get('isPdf') === 'true' ? true : false, } // @ts-ignore const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.findFirst({ @@ -84,7 +86,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ DETAIL_INFO: true, }, }) - if (checkRole(survey, sessionParams)) { + if (sessionParams.isPdf || checkRole(survey, sessionParams)) { return NextResponse.json(survey) } else { return NextResponse.json({ error: '該当物件の照会権限がありません。' }, { status: 403 }) diff --git a/src/components/pdf/SurveySaleDownloadPdf.tsx b/src/components/pdf/SurveySaleDownloadPdf.tsx index 71d2922..0cda0e1 100644 --- a/src/components/pdf/SurveySaleDownloadPdf.tsx +++ b/src/components/pdf/SurveySaleDownloadPdf.tsx @@ -6,20 +6,27 @@ import { useParams, useRouter } from 'next/navigation' import { useSurvey } from '@/hooks/useSurvey' import { radioEtcData, roofMaterial, selectBoxOptions, supplementaryFacilities } from '../survey-sale/detail/RoofForm' import { useSpinnerStore } from '@/store/spinnerStore' +import { useSessionStore } from '@/store/session' export default function SurveySaleDownloadPdf() { const params = useParams() const id = params.id const router = useRouter() - const { surveyDetail, isLoadingSurveyDetail } = useSurvey(Number(id)) + const { surveyDetail, isLoadingSurveyDetail } = useSurvey(Number(id), true) const { setIsShow } = useSpinnerStore() + const { session } = useSessionStore() const targetRef = useRef(null) const isGeneratedRef = useRef(false) useEffect(() => { - if (isLoadingSurveyDetail || !surveyDetail || isGeneratedRef.current) return + if (isLoadingSurveyDetail || isGeneratedRef.current) return + if (surveyDetail === null) { + alert('データが見つかりません。') + router.replace('/') + return + } isGeneratedRef.current = true handleDownPdf() }, [surveyDetail?.id, isLoadingSurveyDetail]) @@ -48,11 +55,19 @@ export default function SurveySaleDownloadPdf() { }, } - generatePDF(targetRef, options).then(() => { - setIsShow(false) - router.replace(`/survey-sale/${id}`) - alert('PDFの生成が完了しました。 ポップアップウィンドウからダウンロードしてください。') - }) + generatePDF(targetRef, options) + .then(() => { + setIsShow(false) + if (session?.isLoggedIn) { + router.replace(`/survey-sale/${id}`) + } else { + router.replace('/') + } + alert('PDFの生成が完了しました。 ポップアップウィンドウからダウンロードしてください。') + }) + .catch((error: any) => { + console.error('error', error) + }) } return ( diff --git a/src/components/survey-sale/detail/DataTable.tsx b/src/components/survey-sale/detail/DataTable.tsx index 30bfc40..b02392e 100644 --- a/src/components/survey-sale/detail/DataTable.tsx +++ b/src/components/survey-sale/detail/DataTable.tsx @@ -4,15 +4,12 @@ import { useSurvey } from '@/hooks/useSurvey' import { useParams, useRouter } from 'next/navigation' import { useEffect } from 'react' import DetailForm from './DetailForm' -import { useSessionStore } from '@/store/session' export default function DataTable() { const params = useParams() const id = params.id const router = useRouter() - const { session } = useSessionStore() - useEffect(() => { if (Number.isNaN(Number(id))) { alert('間違ったアプローチです。') diff --git a/src/components/survey-sale/detail/DetailForm.tsx b/src/components/survey-sale/detail/DetailForm.tsx index 8fd1ead..503f7b6 100644 --- a/src/components/survey-sale/detail/DetailForm.tsx +++ b/src/components/survey-sale/detail/DetailForm.tsx @@ -74,7 +74,7 @@ export default function DetailForm() { const modeset = Number(routeId) ? 'READ' : idParam ? 'EDIT' : 'CREATE' const id = Number(routeId) ? Number(routeId) : Number(idParam) - const { surveyDetail, isLoadingSurveyDetail, validateSurveyDetail } = useSurvey(Number(id)) + const { surveyDetail, isLoadingSurveyDetail } = useSurvey(Number(id)) const { session } = useSessionStore() const [mode, setMode] = useState(modeset) @@ -103,9 +103,8 @@ export default function DetailForm() { })) }, [session?.isLoggedIn]) - // 설문 데이터 로딩 및 업데이트 useEffect(() => { - if (isLoadingSurveyDetail || !session?.isLoggedIn) return + if (isLoadingSurveyDetail) return if (surveyDetail && (mode === 'EDIT' || mode === 'READ')) { const { id, uptDt, regDt, detailInfo, ...rest } = surveyDetail setBasicInfoData((prev) => ({ @@ -116,12 +115,9 @@ export default function DetailForm() { if (detailInfo) { const { id, uptDt, regDt, basicInfoId, ...rest } = detailInfo setRoofInfoData(rest) - if (validateSurveyDetail(rest).trim() !== '') { - // validation logic here if needed - } } } - }, [mode, session?.isLoggedIn, isLoadingSurveyDetail]) + }, [mode, isLoadingSurveyDetail, surveyDetail]) const data = { basic: basicInfoData, diff --git a/src/components/survey-sale/list/ListTable.tsx b/src/components/survey-sale/list/ListTable.tsx index 50030ac..12088fe 100644 --- a/src/components/survey-sale/list/ListTable.tsx +++ b/src/components/survey-sale/list/ListTable.tsx @@ -27,12 +27,7 @@ export default function ListTable() { }, [pathname]) useEffect(() => { - if (!session.isLoggedIn || isLoadingSurveyList) return - // if ('status' in surveyList && surveyList.status === 403) { - // alert('権限がありません。') - // router.push('/survey-sale') - // return - // } + if (isLoadingSurveyList) return if ('count' in surveyList && surveyList.count > 0) { if (offset > 0) { setHeldSurveyList((prev) => [...prev, ...surveyList.data]) @@ -44,7 +39,7 @@ export default function ListTable() { setHeldSurveyList([]) setHasMore(false) } - }, [surveyList, offset, session.isLoggedIn]) + }, [surveyList, offset, isLoadingSurveyList]) const handleDetailClick = (id: number) => { router.push(`/survey-sale/${id}`) diff --git a/src/hooks/useSurvey.ts b/src/hooks/useSurvey.ts index 1fc4994..7fcc6d5 100644 --- a/src/hooks/useSurvey.ts +++ b/src/hooks/useSurvey.ts @@ -55,7 +55,10 @@ type ZipCode = { kana3: string } -export function useSurvey(id?: number): { +export function useSurvey( + id?: number, + isPdf?: boolean, +): { surveyList: { data: SurveyBasicInfo[]; count: number } | {} surveyDetail: SurveyBasicInfo | null isLoadingSurveyList: boolean @@ -102,6 +105,9 @@ export function useSurvey(id?: number): { return false } } + if (isPdf) { + return true + } alert('ログインしていません。') return false } @@ -114,7 +120,7 @@ export function useSurvey(id?: number): { queryKey: ['survey', 'list', keyword, searchOption, isMySurvey, sort, offset, session?.storeNm, session?.builderId, session?.role], queryFn: async () => { if (!checkSession()) { - router.replace('/') + router.replace('/login') return { data: [], count: 0 } } const resp = await axiosInstance(null).get<{ data: SurveyBasicInfo[]; count: number }>('/api/survey-sales', { @@ -131,7 +137,6 @@ export function useSurvey(id?: number): { }) return resp.data }, - enabled: session?.isLoggedIn, }) const surveyData = useMemo(() => { if (!surveyListData) return { count: 0, data: [] } @@ -144,7 +149,7 @@ export function useSurvey(id?: number): { queryKey: ['survey', id], queryFn: async () => { if (!checkSession()) { - router.replace('/survey-sale') + router.replace('/login') return null } if (id === 0 || id === undefined) return null @@ -155,6 +160,7 @@ export function useSurvey(id?: number): { storeId: session?.storeId, builderId: session?.builderId, isLoggedIn: session?.isLoggedIn, + isPdf: isPdf, }, }) return resp.data @@ -164,7 +170,7 @@ export function useSurvey(id?: number): { return null } }, - enabled: id !== 0 && id !== undefined && session?.isLoggedIn, + enabled: id !== 0 && id !== undefined, }) const { mutateAsync: createSurvey, isPending: isCreatingSurvey } = useMutation({ From 7db421c26a8024d9497ad9a6ea643a0b5c594527 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Tue, 10 Jun 2025 15:32:27 +0900 Subject: [PATCH 4/5] refactor: Refactoring code and add error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 조사매물 조회 시 로그인 여부 확인 로직 서버 사이드로 이동 - 에러 핸들링 추가 - 조사매물 수정/작성 페이지 컴포넌트 리팩토링 - 조사매물 작성 후 제출 버튼 클릭 시 node 삽입 오류 해결 --- src/app/api/qna/list/route.ts | 11 ++ src/app/api/survey-sales/[id]/route.ts | 131 ++++++++---------- src/app/api/survey-sales/route.ts | 25 +++- src/app/survey-sale/@navTab/default.tsx | 5 - src/app/survey-sale/[id]/page.tsx | 4 +- src/app/survey-sale/layout.tsx | 3 +- .../popup/SurveySaleSubmitPopup.tsx | 3 +- src/components/survey-sale/common/NavTab.tsx | 90 ------------ .../survey-sale/detail/BasicForm.tsx | 6 +- .../survey-sale/detail/ButtonForm.tsx | 57 ++++---- .../survey-sale/detail/DataTable.tsx | 26 +--- .../survey-sale/detail/DetailForm.tsx | 27 +++- src/hooks/useInquiry.ts | 35 ++++- src/hooks/useSurvey.ts | 124 ++++++++--------- src/types/Survey.ts | 2 +- 15 files changed, 242 insertions(+), 307 deletions(-) delete mode 100644 src/app/survey-sale/@navTab/default.tsx delete mode 100644 src/components/survey-sale/common/NavTab.tsx diff --git a/src/app/api/qna/list/route.ts b/src/app/api/qna/list/route.ts index f793b98..9708e4f 100644 --- a/src/app/api/qna/list/route.ts +++ b/src/app/api/qna/list/route.ts @@ -1,8 +1,19 @@ import axios from 'axios' import { NextResponse } from 'next/server' import { queryStringFormatter } from '@/utils/common-utils' +import { getIronSession } from 'iron-session' +import { cookies } from 'next/headers' +import { sessionOptions } from '@/libs/session' +import { SessionData } from '@/types/Auth' export async function GET(request: Request) { + const cookieStore = await cookies() + const session = await getIronSession(cookieStore, sessionOptions) + + if (!session.isLoggedIn) { + return NextResponse.json({ error: 'ログインしていません。' }, { status: 401 }) + } + const { searchParams } = new URL(request.url) const params: Record = {} diff --git a/src/app/api/survey-sales/[id]/route.ts b/src/app/api/survey-sales/[id]/route.ts index 8d7806c..5c23cd4 100644 --- a/src/app/api/survey-sales/[id]/route.ts +++ b/src/app/api/survey-sales/[id]/route.ts @@ -1,99 +1,86 @@ import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/libs/prisma' import { convertToSnakeCase } from '@/utils/common-utils' +import { getIronSession } from 'iron-session' +import { sessionOptions } from '@/libs/session' +import { cookies } from 'next/headers' +import type { SessionData } from '@/types/Auth' -interface Survey { - SRL_NO: string - SUBMISSION_STATUS: boolean - SUBMISSION_TARGET_ID: string | null - STORE_ID: string | null - CONSTRUCTION_POINT_ID: string | null -} +const ERROR_MESSAGES = { + NOT_FOUND: 'データが見つかりません。', + UNAUTHORIZED: 'Unauthorized', + NO_PERMISSION: '該当物件の照会権限がありません。', + FETCH_ERROR: 'データの取得に失敗しました。', +} as const -interface SessionParams { - role: string | null - storeId: string | null - builderId: string | null - isLoggedIn: string | null - isPdf: boolean | null -} +// Role check functions +const checkT01Role = (survey: any): boolean => survey.SRL_NO !== '一時保存' -const checkT01Role = (survey: Survey): boolean => survey.SRL_NO !== '一時保存' - -const checkAdminRole = (survey: Survey, storeId: string | null): boolean => { +const checkAdminRole = (survey: any, storeId: string | null): boolean => { if (!storeId) return false - - if (survey.SUBMISSION_STATUS) { - return survey.SUBMISSION_TARGET_ID === storeId || survey.STORE_ID === storeId - } - return survey.STORE_ID === storeId + return survey.SUBMISSION_STATUS ? survey.SUBMISSION_TARGET_ID === storeId || survey.STORE_ID === storeId : survey.STORE_ID === storeId } -const checkAdminSubRole = (survey: Survey, storeId: string | null): boolean => { +const checkAdminSubRole = (survey: any, storeId: string | null): boolean => { if (!storeId) return false - - if (survey.SUBMISSION_STATUS) { - return survey.SUBMISSION_TARGET_ID === storeId || (survey.STORE_ID === storeId && survey.CONSTRUCTION_POINT_ID === null) - } - return survey.STORE_ID === storeId && survey.CONSTRUCTION_POINT_ID === null + return survey.SUBMISSION_STATUS + ? survey.SUBMISSION_TARGET_ID === storeId || (survey.STORE_ID === storeId && !survey.CONSTRUCTION_POINT_ID) + : survey.STORE_ID === storeId && !survey.CONSTRUCTION_POINT_ID } -const checkPartnerOrBuilderRole = (survey: Survey, builderId: string | null): boolean => { +const checkPartnerOrBuilderRole = (survey: any, builderId: string | null): boolean => { if (!builderId) return false return survey.CONSTRUCTION_POINT_ID === builderId } -const checkRole = (survey: Survey, sessionParams: SessionParams): boolean => { - if (!survey || !sessionParams.role) return false +const checkRole = (survey: any, session: any): boolean => { + if (!survey || !session.isLoggedIn) return false - switch (sessionParams.role) { - case 'T01': - return checkT01Role(survey) - // T01 이외 1차점 - case 'Admin': - return checkAdminRole(survey, sessionParams.storeId) - // 2차점 - case 'Admin_Sub': - return checkAdminSubRole(survey, sessionParams.storeId) - // partner - case 'Partner': - // 2차점 시공권한 user - case 'Builder': - return checkPartnerOrBuilderRole(survey, sessionParams.builderId) - default: - return false + const roleChecks = { + T01: () => checkT01Role(survey), + Admin: () => checkAdminRole(survey, session.storeId), + Admin_Sub: () => checkAdminSubRole(survey, session.storeId), + Partner: () => checkPartnerOrBuilderRole(survey, session.builderId), + Builder: () => checkPartnerOrBuilderRole(survey, session.builderId), } + + return roleChecks[session.role as keyof typeof roleChecks]?.() ?? false } +const fetchSurvey = async (id: number) => { + // @ts-ignore + return await prisma.SD_SURVEY_SALES_BASIC_INFO.findFirst({ + where: { ID: id }, + include: { DETAIL_INFO: true }, + }) +} + +// API Route Handlers export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { + const cookieStore = await cookies() + const session = await getIronSession(cookieStore, sessionOptions) const { id } = await params const { searchParams } = new URL(request.url) + const isPdf = searchParams.get('isPdf') === 'true' - const sessionParams: SessionParams = { - role: searchParams.get('role'), - storeId: searchParams.get('storeId'), - builderId: searchParams.get('builderId'), - isLoggedIn: searchParams.get('isLoggedIn'), - isPdf: searchParams.get('isPdf') === 'true' ? true : false, + const survey = await fetchSurvey(Number(id)) + if (!survey) { + return NextResponse.json({ error: ERROR_MESSAGES.NOT_FOUND }, { status: 404 }) } - // @ts-ignore - const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.findFirst({ - where: { - ID: Number(id), - }, - include: { - DETAIL_INFO: true, - }, - }) - if (sessionParams.isPdf || checkRole(survey, sessionParams)) { + + if (isPdf || checkRole(survey, session)) { return NextResponse.json(survey) - } else { - return NextResponse.json({ error: '該当物件の照会権限がありません。' }, { status: 403 }) } - } catch (error: any) { + + if (!session?.isLoggedIn || session?.role === null) { + return NextResponse.json({ error: ERROR_MESSAGES.UNAUTHORIZED }, { status: 401 }) + } + + return NextResponse.json({ error: ERROR_MESSAGES.NO_PERMISSION }, { status: 403 }) + } catch (error) { console.error('Error fetching survey:', error) - return NextResponse.json({ error: 'データの取得に失敗しました。' }, { status: 500 }) + return NextResponse.json({ error: ERROR_MESSAGES.FETCH_ERROR }, { status: 500 }) } } @@ -106,7 +93,7 @@ const getNewSrlNo = async (srlNo: string, storeId: string, role: string) => { const lastSurvey = await prisma.SD_SURVEY_SALES_BASIC_INFO.findFirst({ where: { SRL_NO: { - startsWith: srlRole + storeId, + startsWith: srlRole, }, }, orderBy: { @@ -152,7 +139,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json(survey) } catch (error) { console.error('Error updating survey:', error) - return NextResponse.json({ error: 'Failed to update survey' }, { status: 500 }) + return NextResponse.json({ error: 'データ保存に失敗しました。' }, { status: 500 }) } } @@ -182,10 +169,10 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise }) }) - return NextResponse.json({ message: 'Survey deleted successfully' }) + return NextResponse.json({ message: 'success' }) } catch (error) { console.error('Error deleting survey:', error) - return NextResponse.json({ error: 'Failed to delete survey' }, { status: 500 }) + return NextResponse.json({ error: 'データ削除に失敗しました。' }, { status: 500 }) } } @@ -207,6 +194,6 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< return NextResponse.json({ message: 'Survey confirmed successfully', data: survey }) } catch (error) { console.error('Error updating survey:', error) - return NextResponse.json({ error: 'Failed to update survey' }, { status: 500 }) + return NextResponse.json({ error: 'データ保存に失敗しました。' }, { status: 500 }) } } diff --git a/src/app/api/survey-sales/route.ts b/src/app/api/survey-sales/route.ts index 39c8f02..dfb486c 100644 --- a/src/app/api/survey-sales/route.ts +++ b/src/app/api/survey-sales/route.ts @@ -135,6 +135,21 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => { return where } +const checkSession = (params: SearchParams) => { + if (params.role === null) { + return NextResponse.json({ data: [], count: 0 }) + } + if (params.role === 'Builder' || params.role === 'Partner') { + if (params.builderId === null) { + return NextResponse.json({ data: [], count: 0 }) + } + } else { + if (params.storeId === null) { + return NextResponse.json({ data: [], count: 0 }) + } + } + return null +} /** * GET 핸들러 - 설문 목록 조회 @@ -154,6 +169,12 @@ export async function GET(request: Request) { builderId: searchParams.get('builderId'), } + // 세션 체크 결과 처리 + const sessionCheckResult = checkSession(params) + if (sessionCheckResult) { + return sessionCheckResult + } + // 검색 조건 구성 const where: WhereCondition = { AND: [] } @@ -186,7 +207,7 @@ export async function GET(request: Request) { return NextResponse.json({ data: { data: surveys, count: count } }) } catch (error) { console.error(error) - return NextResponse.json({ error: 'Fail Read Survey' }, { status: 500 }) + return NextResponse.json({ error: 'データ照会に失敗しました。' }, { status: 500 }) } } @@ -273,6 +294,6 @@ export async function POST(request: Request) { return NextResponse.json(result) } catch (error) { console.error(error) - return NextResponse.json({ error: 'Fail Create Survey' }, { status: 500 }) + return NextResponse.json({ error: 'データ保存に失敗しました。' }, { status: 500 }) } } diff --git a/src/app/survey-sale/@navTab/default.tsx b/src/app/survey-sale/@navTab/default.tsx deleted file mode 100644 index 6fefef5..0000000 --- a/src/app/survey-sale/@navTab/default.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import NavTab from '@/components/survey-sale/common/NavTab' - -export default function page() { - return -} diff --git a/src/app/survey-sale/[id]/page.tsx b/src/app/survey-sale/[id]/page.tsx index acbb03e..392ea19 100644 --- a/src/app/survey-sale/[id]/page.tsx +++ b/src/app/survey-sale/[id]/page.tsx @@ -1,9 +1,9 @@ -import DataTable from '@/components/survey-sale/detail/DataTable' +import DetailForm from '@/components/survey-sale/detail/DetailForm' export default function page() { return ( <> - + ) } diff --git a/src/app/survey-sale/layout.tsx b/src/app/survey-sale/layout.tsx index 83f37c0..6e7278a 100644 --- a/src/app/survey-sale/layout.tsx +++ b/src/app/survey-sale/layout.tsx @@ -2,10 +2,9 @@ import type { ReactNode } from 'react' interface SurveySaleLayoutProps { children: ReactNode - navTab: ReactNode } -export default function layout({ children, navTab }: SurveySaleLayoutProps) { +export default function layout({ children }: SurveySaleLayoutProps) { return ( <>
diff --git a/src/components/popup/SurveySaleSubmitPopup.tsx b/src/components/popup/SurveySaleSubmitPopup.tsx index ce6be9e..bb4cdfe 100644 --- a/src/components/popup/SurveySaleSubmitPopup.tsx +++ b/src/components/popup/SurveySaleSubmitPopup.tsx @@ -123,7 +123,7 @@ export default function SurveySaleSubmitPopup() { .then(() => { if (!isSubmittingSurvey) { alert('提出が完了しました。') - // submitSurvey({ targetId: submitData.targetId, targetNm: submitData.targetNm }) + submitSurvey({ targetId: submitData.targetId, targetNm: submitData.targetNm }) popupController.setSurveySaleSubmitPopup(false) } }) @@ -132,7 +132,6 @@ export default function SurveySaleSubmitPopup() { alert('メール送信に失敗しました。 再度送信してください。') }) .finally(() => { - submitSurvey({ targetId: submitData.targetId, targetNm: submitData.targetNm }) setIsShow(false) popupController.setSurveySaleSubmitPopup(false) }) diff --git a/src/components/survey-sale/common/NavTab.tsx b/src/components/survey-sale/common/NavTab.tsx deleted file mode 100644 index ffa6ea7..0000000 --- a/src/components/survey-sale/common/NavTab.tsx +++ /dev/null @@ -1,90 +0,0 @@ -'use client' - -import { useSurveySaleTabState } from '@/store/surveySaleTabState' -import { usePathname, useRouter, useSearchParams, useParams } from 'next/navigation' -import { useEffect } from 'react' - -export default function NavTab() { - const router = useRouter() - const pathname = usePathname() - - const searchParams = useSearchParams() - const id = searchParams.get('id') - const isTemp = searchParams.get('isTemp') - - const params = useParams() - const detailId = params.id - - const { basicInfoSelected, roofInfoSelected, reset, setBasicInfoSelected, setRoofInfoSelected } = useSurveySaleTabState() - - useEffect(() => { - return () => { - reset() - } - }, [reset]) - - if (pathname === '/survey-sale') { - return null - } - - const scrollSection = (section: string) => { - const sectionElement = document.getElementById(section) - if (sectionElement) { - sectionElement.scrollIntoView({ behavior: 'smooth' }) - } - } - - const handleBasicInfoClick = () => { - // if (id) { - // router.push(`/survey-sale/basic-info?id=${id}`) - // return - // } - if (detailId) { - router.push(`/survey-sale/${detailId}`) - return - } - scrollSection('basic-form-section') - - setBasicInfoSelected() - } - - const handleRoofInfoClick = () => { - // if (id) { - // if (isTemp === 'true') { - // alert('基本情報が一時保存された状態です。') - // return - // } - // router.push(`/survey-sale/roof-info?id=${id}`) - // return - // } - if (detailId) { - router.push(`/survey-sale/${detailId}?tab=roof-info`) - return - } - if (pathname === '/survey-sale/basic-info') { - alert('基本情報を先に保存してください。') - return null - } - // if (pathname === '/survey-sale/regist') { - scrollSection('roof-form-section') - // } - setRoofInfoSelected() - } - - return ( - <> -
-
-
- - -
-
-
- - ) -} diff --git a/src/components/survey-sale/detail/BasicForm.tsx b/src/components/survey-sale/detail/BasicForm.tsx index 10ce000..520393d 100644 --- a/src/components/survey-sale/detail/BasicForm.tsx +++ b/src/components/survey-sale/detail/BasicForm.tsx @@ -18,7 +18,7 @@ interface BasicFormProps { export default function BasicForm({ basicInfo, setBasicInfo, mode, session }: BasicFormProps) { const { setBasicInfoSelected } = useSurveySaleTabState() const [isFlip, setIsFlip] = useState(true) - const { addressData } = useAddressStore() + const { addressData, resetAddressData } = useAddressStore() const popupController = usePopupController() useEffect(() => { @@ -28,13 +28,13 @@ export default function BasicForm({ basicInfo, setBasicInfo, mode, session }: Ba // 주소 데이터가 변경될 때만 업데이트 useEffect(() => { if (!addressData) return - setBasicInfo({ ...basicInfo, - postCode: addressData.post_code, + postCode: addressData.post_code.slice(0, 3) + '-' + addressData.post_code.slice(3), address: addressData.address, addressDetail: addressData.address_detail, }) + resetAddressData() }, [addressData]) return ( diff --git a/src/components/survey-sale/detail/ButtonForm.tsx b/src/components/survey-sale/detail/ButtonForm.tsx index 2088629..d452aec 100644 --- a/src/components/survey-sale/detail/ButtonForm.tsx +++ b/src/components/survey-sale/detail/ButtonForm.tsx @@ -29,10 +29,9 @@ interface SaveData extends SurveyBasicRequest { export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { const router = useRouter() const { session } = useSessionStore() - const searchParams = useSearchParams() - const idParam = searchParams.get('id') + const params = useParams() - const routeId = params.id + const id = Number(params.id) const popupController = usePopupController() const [saveData, setSaveData] = useState({ @@ -47,7 +46,6 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { }) const isSubmit = data.basic.submissionStatus - const id = Number(routeId) ? Number(routeId) : Number(idParam) const { deleteSurvey, updateSurvey, isDeletingSurvey, isUpdatingSurvey } = useSurvey(id) const { validateSurveyDetail, createSurvey, isCreatingSurvey } = useSurvey() @@ -66,7 +64,7 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { const calculatePermissions = (session: any, basicData: SurveyBasicRequest): PermissionState => { const isSubmiter = calculateSubmitPermission(session, basicData) - const isWriter = session.userNm === basicData.representative + const isWriter = session.userId === basicData.representativeId const isReceiver = session?.storeId === basicData.submissionTargetId return { isSubmiter, isWriter, isReceiver } @@ -99,19 +97,19 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { } const tempSaveProcess = async () => { - if (idParam) { + if (!Number.isNaN(id)) { await updateSurvey({ survey: saveData, isTemporary: true }) if (!isUpdatingSurvey) { - router.push(`/survey-sale/${idParam}`) + setMode('READ') } } else { const updatedData = { ...saveData, srlNo: '一時保存', } - const id = await createSurvey(updatedData) + const savedId = await createSurvey(updatedData) if (!isCreatingSurvey) { - router.push(`/survey-sale/${id}`) + router.push(`/survey-sale/${savedId}`) } } alert('一時保存されました。') @@ -131,25 +129,24 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { } const handleSuccessfulSave = async (isSubmitProcess?: boolean) => { - if (idParam) { - await updateSurvey({ survey: saveData, isTemporary: false, storeId: session.storeId ?? '' }) + if (!Number.isNaN(id)) { + await updateSurvey({ + survey: saveData, + isTemporary: false, + storeId: session?.role === 'Partner' ? session?.builderId ?? null : session?.storeId ?? null, + }) if (!isUpdatingSurvey) { - router.push(`/survey-sale/${idParam}`) + setMode('READ') } } else { - const id = await createSurvey(saveData) - if (!isCreatingSurvey) { - router.push(`/survey-sale/${id}`) + const savedId = await createSurvey(saveData) + if (isSubmitProcess) { + await router.push(`/survey-sale/${savedId}?show=true`) + } else { + await router.push(`/survey-sale/${savedId}`) + alert('保存されました。') } } - - if (isSubmitProcess) { - if (!isCreatingSurvey && !isUpdatingSurvey) { - await popupController.setSurveySaleSubmitPopup(true) - } - } else { - alert('保存されました。') - } } const handleFailedSave = (emptyField: string | null) => { @@ -162,7 +159,7 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { } const handleDelete = async () => { - if (routeId) { + if (!Number.isNaN(id)) { window.neoConfirm('削除しますか?', async () => { await deleteSurvey() if (!isDeletingSurvey) { @@ -174,12 +171,12 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { } const handleSubmit = async () => { - if (data.basic.srlNo?.startsWith('一時保存') && Number(routeId)) { + if (data.basic.srlNo?.startsWith('一時保存') && Number.isNaN(id)) { alert('一時保存されたデータは提出できません。') return } - if (Number(routeId)) { + if (mode === 'READ') { window.neoConfirm('提出しますか?', async () => { popupController.setSurveySaleSubmitPopup(true) }) @@ -208,9 +205,7 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) {
- {(permissions.isWriter || permissions.isSubmiter || (permissions.isReceiver && isSubmit)) && ( - - )} + {(permissions.isWriter || permissions.isSubmiter || (permissions.isReceiver && isSubmit)) && } {(permissions.isWriter || (permissions.isReceiver && isSubmit)) && } {!isSubmit && permissions.isSubmiter && }
@@ -243,14 +238,12 @@ const ListButton = () => { ) } -const EditButton = ({ setMode, id }: { setMode: (mode: Mode) => void; id: string }) => { - const router = useRouter() +const EditButton = ({ setMode }: { setMode: (mode: Mode) => void }) => { return (
@@ -92,7 +75,6 @@ export default function DataTable() {
- ) } diff --git a/src/components/survey-sale/detail/DetailForm.tsx b/src/components/survey-sale/detail/DetailForm.tsx index 503f7b6..b4a995a 100644 --- a/src/components/survey-sale/detail/DetailForm.tsx +++ b/src/components/survey-sale/detail/DetailForm.tsx @@ -5,9 +5,11 @@ import { useEffect, useState } from 'react' import ButtonForm from './ButtonForm' import BasicForm from './BasicForm' import RoofForm from './RoofForm' -import { useParams, useSearchParams } from 'next/navigation' +import { useParams, useRouter, useSearchParams, usePathname } from 'next/navigation' import { useSurvey } from '@/hooks/useSurvey' import { useSessionStore } from '@/store/session' +import DataTable from './DataTable' +import { usePopupController } from '@/store/popupController' const roofInfoForm: SurveyDetailRequest = { contractCapacity: null, @@ -68,16 +70,19 @@ const basicInfoForm: SurveyBasicRequest = { } export default function DetailForm() { - const idParam = useSearchParams().get('id') - const routeId = useParams().id + const id = useParams().id - const modeset = Number(routeId) ? 'READ' : idParam ? 'EDIT' : 'CREATE' - const id = Number(routeId) ? Number(routeId) : Number(idParam) + const modeset = !Number.isNaN(Number(id)) ? 'READ' : 'CREATE' const { surveyDetail, isLoadingSurveyDetail } = useSurvey(Number(id)) const { session } = useSessionStore() + const searchParams = useSearchParams() + const popupController = usePopupController() + const router = useRouter() + const pathname = usePathname() const [mode, setMode] = useState(modeset) + const [basicInfoData, setBasicInfoData] = useState(() => ({ ...basicInfoForm, representative: session?.userNm ?? '', @@ -89,6 +94,14 @@ export default function DetailForm() { })) const [roofInfoData, setRoofInfoData] = useState(roofInfoForm) + useEffect(() => { + const show = searchParams.get('show') + if (show === 'true') { + popupController.setSurveySaleSubmitPopup(true) + router.replace(pathname) + } + }, [searchParams, pathname]) + // 세션 데이터가 변경될 때 기본 정보 업데이트 useEffect(() => { if (!session?.isLoggedIn) return @@ -104,8 +117,7 @@ export default function DetailForm() { }, [session?.isLoggedIn]) useEffect(() => { - if (isLoadingSurveyDetail) return - if (surveyDetail && (mode === 'EDIT' || mode === 'READ')) { + if (!isLoadingSurveyDetail && surveyDetail && (mode === 'EDIT' || mode === 'READ')) { const { id, uptDt, regDt, detailInfo, ...rest } = surveyDetail setBasicInfoData((prev) => ({ ...prev, @@ -128,6 +140,7 @@ export default function DetailForm() { return ( <> + {mode === 'READ' && surveyDetail && }
diff --git a/src/hooks/useInquiry.ts b/src/hooks/useInquiry.ts index 23b433e..7bec432 100644 --- a/src/hooks/useInquiry.ts +++ b/src/hooks/useInquiry.ts @@ -4,6 +4,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useInquiryFilterStore } from '@/store/inquiryFilterStore' import { useMemo } from 'react' import { useSessionStore } from '@/store/session' +import { useRouter } from 'next/navigation' export function useInquiry( qnoNo?: number, @@ -22,6 +23,32 @@ export function useInquiry( const { inquiryListRequest, offset } = useInquiryFilterStore() const { session } = useSessionStore() const { axiosInstance } = useAxios() + const router = useRouter() + + const errorRouter = (error: any) => { + const status = error.response?.status + alert(error.response?.data.error) + switch (status) { + // session 없는 경우 + case 401: + router.replace('/login') + break + // 조회 권한 없는 경우 + case 403: + router.replace('/inquiry/list') + break + // 데이터 DB상 존재하지 않는 경우 + case 404: + router.replace('/inquiry/list') + break + // 서버 오류 + case 500: + router.back() + break + default: + break + } + } const { data: inquiryList, isLoading: isLoadingInquiryList } = useQuery({ queryKey: ['inquiryList', inquiryListRequest, offset], @@ -32,7 +59,7 @@ export function useInquiry( }) return resp.data.data } catch (error: any) { - console.error(error.response.data) + errorRouter(error) return [] } }, @@ -57,7 +84,7 @@ export function useInquiry( }) return resp.data.data } catch (error: any) { - console.error(error.response) + errorRouter(error) return null } }, @@ -72,6 +99,9 @@ export function useInquiry( onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['inquiryList'] }) }, + onError: (error: any) => { + errorRouter(error) + }, }) const downloadFile = async (encodeFileNo: number, srcFileNm: string) => { @@ -90,7 +120,6 @@ export function useInquiry( return blob } catch (error) { - console.error('File download error:', error) alert('ファイルのダウンロードに失敗しました') return null } diff --git a/src/hooks/useSurvey.ts b/src/hooks/useSurvey.ts index 7fcc6d5..bafe8ab 100644 --- a/src/hooks/useSurvey.ts +++ b/src/hooks/useSurvey.ts @@ -6,6 +6,7 @@ import { useSessionStore } from '@/store/session' import { useAxios } from './useAxios' import { queryStringFormatter } from '@/utils/common-utils' import { useRouter } from 'next/navigation' +import { usePopupController } from '@/store/popupController' export const requiredFields = [ { @@ -68,12 +69,13 @@ export function useSurvey( isDeletingSurvey: boolean isSubmittingSurvey: boolean createSurvey: (survey: SurveyRegistRequest) => Promise - updateSurvey: ({ survey, isTemporary, storeId }: { survey: SurveyRegistRequest; isTemporary: boolean; storeId?: string }) => void + updateSurvey: ({ survey, isTemporary, storeId }: { survey: SurveyRegistRequest; isTemporary: boolean; storeId?: string | null }) => void deleteSurvey: () => Promise submitSurvey: (params: { targetId?: string | null; targetNm?: string | null }) => void validateSurveyDetail: (surveyDetail: SurveyDetailRequest) => string getZipCode: (zipCode: string) => Promise refetchSurveyList: () => void + refetchSurveyDetail: () => void getSubmitTarget: (params: { storeId: string; role: string }) => Promise } { const queryClient = useQueryClient() @@ -82,34 +84,29 @@ export function useSurvey( const { axiosInstance } = useAxios() const router = useRouter() - const checkSession = () => { - if (session?.isLoggedIn) { - switch (session?.role) { - case 'T01': - case 'Admin': - case 'Admin_Sub': - if (session?.storeId === null) { - alert('販売店IDがありません。') - return false - } - return true - case 'Builder': - case 'Partner': - if (session?.builderId === null) { - alert('施工店IDがありません。') - return false - } - return true - default: - alert('権限が間違っています。') - return false - } + const errorRouter = (error: any) => { + const status = error.response?.status + alert(error.response?.data.error) + switch (status) { + // session 없는 경우 + case 401: + router.replace('/login') + break + // 조회 권한 없는 경우 + case 403: + router.replace('/survey-sale') + break + // 데이터 DB상 존재하지 않는 경우 + case 404: + router.replace('/survey-sale') + break + // 서버 오류 + case 500: + router.back() + break + default: + break } - if (isPdf) { - return true - } - alert('ログインしていません。') - return false } const { @@ -119,23 +116,24 @@ export function useSurvey( } = useQuery({ queryKey: ['survey', 'list', keyword, searchOption, isMySurvey, sort, offset, session?.storeNm, session?.builderId, session?.role], queryFn: async () => { - if (!checkSession()) { - router.replace('/login') + try { + const resp = await axiosInstance(null).get<{ data: SurveyBasicInfo[]; count: number }>('/api/survey-sales', { + params: { + keyword, + searchOption, + isMySurvey, + sort, + offset, + storeId: session?.storeId, + builderId: session?.builderId, + role: session?.role, + }, + }) + return resp.data + } catch (error: any) { + errorRouter(error) return { data: [], count: 0 } } - const resp = await axiosInstance(null).get<{ data: SurveyBasicInfo[]; count: number }>('/api/survey-sales', { - params: { - keyword, - searchOption, - isMySurvey, - sort, - offset, - storeId: session?.storeId, - builderId: session?.builderId, - role: session?.role, - }, - }) - return resp.data }, }) const surveyData = useMemo(() => { @@ -145,39 +143,34 @@ export function useSurvey( } }, [surveyListData]) - const { data: surveyDetail, isLoading: isLoadingSurveyDetail } = useQuery({ + const { + data: surveyDetail, + isLoading: isLoadingSurveyDetail, + refetch: refetchSurveyDetail, + } = useQuery({ queryKey: ['survey', id], queryFn: async () => { - if (!checkSession()) { - router.replace('/login') - return null - } - if (id === 0 || id === undefined) return null + if (Number.isNaN(id) || id === undefined || id === 0) return null try { const resp = await axiosInstance(null).get(`/api/survey-sales/${id}`, { params: { - role: session?.role, - storeId: session?.storeId, - builderId: session?.builderId, - isLoggedIn: session?.isLoggedIn, isPdf: isPdf, }, }) return resp.data } catch (error: any) { - alert(error.response?.data.error) - router.replace('/survey-sale') + errorRouter(error) return null } }, - enabled: id !== 0 && id !== undefined, + enabled: id !== 0 && id !== undefined && id !== null, }) const { mutateAsync: createSurvey, isPending: isCreatingSurvey } = useMutation({ mutationFn: async (survey: SurveyRegistRequest) => { - const resp = await axiosInstance(null).post('/api/survey-sales', { + const resp = await axiosInstance(null).post<{ id: number }>('/api/survey-sales', { survey: survey, - storeId: session?.storeId ?? null, + storeId: session?.role === 'Partner' ? session?.builderId ?? null : session?.storeId ?? null, role: session?.role ?? null, }) return resp.data.id ?? 0 @@ -189,7 +182,7 @@ export function useSurvey( }) const { mutate: updateSurvey, isPending: isUpdatingSurvey } = useMutation({ - mutationFn: async ({ survey, isTemporary, storeId }: { survey: SurveyRegistRequest; isTemporary: boolean; storeId?: string }) => { + mutationFn: async ({ survey, isTemporary, storeId }: { survey: SurveyRegistRequest; isTemporary: boolean; storeId?: string | null }) => { if (id === undefined) throw new Error('id is required') const resp = await axiosInstance(null).put(`/api/survey-sales/${id}`, { survey: survey, @@ -213,7 +206,9 @@ export function useSurvey( }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['survey', 'list'] }) - queryClient.invalidateQueries({ queryKey: ['survey', id] }) + }, + onError: (error: any) => { + alert(error.response?.data.error) }, }) @@ -230,15 +225,16 @@ export function useSurvey( queryClient.invalidateQueries({ queryKey: ['survey', 'list'] }) queryClient.invalidateQueries({ queryKey: ['survey', id] }) }, + onError: (error: any) => { + alert(error.response?.data.error) + }, }) const validateSurveyDetail = (surveyDetail: SurveyDetailRequest) => { - // 상수 정의 const ETC_FIELDS = ['installationSystem', 'rafterSize', 'rafterPitch', 'waterproofMaterial', 'structureOrder'] as const const SPECIAL_CONDITIONS = ['constructionYear', 'insulationPresence'] as const - // 유틸리티 함수들 const isEmptyValue = (value: any): boolean => { return value === null || value?.toString().trim() === '' } @@ -306,8 +302,7 @@ export function useSurvey( const { data } = await axiosInstance(null).get(endpoint) return data } catch (error: any) { - console.error('Failed to fetch submit target:', error) - alert(error.response?.data.error || 'データの取得に失敗しました。') + alert(error.response?.data.error) return null } } @@ -329,5 +324,6 @@ export function useSurvey( getZipCode, getSubmitTarget, refetchSurveyList, + refetchSurveyDetail, } } diff --git a/src/types/Survey.ts b/src/types/Survey.ts index 7128926..312c96b 100644 --- a/src/types/Survey.ts +++ b/src/types/Survey.ts @@ -145,7 +145,7 @@ export type SurveyRegistRequest = { srlNo: string | null //판매점IDyyMMdd000 } -export type Mode = 'CREATE' | 'EDIT' | 'READ' | 'TEMP' // 등록 | 수정 | 상세 | 임시저장 +export type Mode = 'CREATE' | 'EDIT' | 'READ' | 'SUBMIT' // 등록 | 수정 | 상세 | 제출 export type SubmitTargetResponse = { targetStoreId: string From f51b03cab742a504d3f076c440e555295e00dcc1 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Tue, 10 Jun 2025 17:13:35 +0900 Subject: [PATCH 5/5] docs: add annotations in SurveySale related Files - Added detailed descriptions for search parameters and API functionalities in survey-sales routes. - Improved documentation for inquiry-related hooks and types, enhancing clarity on their usage. - Refactored comments for better readability and consistency across the codebase. --- src/app/api/survey-sales/[id]/route.ts | 172 ++++++++++++- src/app/api/survey-sales/route.ts | 208 ++++++++++----- src/components/inquiry/RegistForm.tsx | 4 + src/components/inquiry/list/ListTable.tsx | 2 + src/components/pdf/SurveySaleDownloadPdf.tsx | 6 +- .../popup/SurveySaleSubmitPopup.tsx | 4 +- .../survey-sale/detail/BasicForm.tsx | 10 +- .../survey-sale/detail/ButtonForm.tsx | 25 +- .../survey-sale/detail/DataTable.tsx | 1 + .../survey-sale/detail/DetailForm.tsx | 4 +- .../survey-sale/detail/RoofForm.tsx | 117 +++++++-- .../survey-sale/list/SearchForm.tsx | 1 + src/hooks/useInquiry.ts | 61 +++++ src/hooks/useSurvey.ts | 107 +++++++- src/store/inquiryFilterStore.ts | 12 + src/store/surveyFilterStore.ts | 39 +++ src/store/surveySaleTabState.ts | 26 -- src/types/Inquiry.ts | 236 ++++++++++++------ src/types/Survey.ts | 167 ++++++++++++- 19 files changed, 999 insertions(+), 203 deletions(-) delete mode 100644 src/store/surveySaleTabState.ts diff --git a/src/app/api/survey-sales/[id]/route.ts b/src/app/api/survey-sales/[id]/route.ts index 5c23cd4..61a7b1c 100644 --- a/src/app/api/survey-sales/[id]/route.ts +++ b/src/app/api/survey-sales/[id]/route.ts @@ -6,6 +6,9 @@ import { sessionOptions } from '@/libs/session' import { cookies } from 'next/headers' import type { SessionData } from '@/types/Auth' +/** + * @description 조사 매물 조회 에러 메시지 + */ const ERROR_MESSAGES = { NOT_FOUND: 'データが見つかりません。', UNAUTHORIZED: 'Unauthorized', @@ -13,14 +16,30 @@ const ERROR_MESSAGES = { FETCH_ERROR: 'データの取得に失敗しました。', } as const -// Role check functions +/** + * @description T01 조회 권한 체크 + * @param {any} survey 조사 매물 데이터 + * @returns {boolean} 조사 매물 임시 저장 여부 + */ const checkT01Role = (survey: any): boolean => survey.SRL_NO !== '一時保存' +/** + * @description Admin (1차 판매점) 권한 체크 + * @param {any} survey 조사 매물 데이터 + * @param {string | null} storeId 판매점 ID + * @returns {boolean} 권한 존재 여부 + */ const checkAdminRole = (survey: any, storeId: string | null): boolean => { if (!storeId) return false return survey.SUBMISSION_STATUS ? survey.SUBMISSION_TARGET_ID === storeId || survey.STORE_ID === storeId : survey.STORE_ID === storeId } +/** + * @description Admin_Sub (2차 판매점) 권한 체크 + * @param {any} survey 조사 매물 데이터 + * @param {string | null} storeId 판매점 ID + * @returns {boolean} 권한 존재 여부 + */ const checkAdminSubRole = (survey: any, storeId: string | null): boolean => { if (!storeId) return false return survey.SUBMISSION_STATUS @@ -28,11 +47,23 @@ const checkAdminSubRole = (survey: any, storeId: string | null): boolean => { : survey.STORE_ID === storeId && !survey.CONSTRUCTION_POINT_ID } +/** + * @description Partner (파트너) 또는 Builder (2차 판매점의 시공권한 회원) 권한 체크 + * @param {any} survey 조사 매물 데이터 + * @param {string | null} builderId 시공점 ID + * @returns {boolean} 권한 존재 여부 + */ const checkPartnerOrBuilderRole = (survey: any, builderId: string | null): boolean => { if (!builderId) return false return survey.CONSTRUCTION_POINT_ID === builderId } +/** + * @description 권한 체크 + * @param {any} survey 조사 매물 데이터 + * @param {any} session 세션 데이터 + * @returns {boolean} 권한 존재 여부 + */ const checkRole = (survey: any, session: any): boolean => { if (!survey || !session.isLoggedIn) return false @@ -47,6 +78,11 @@ const checkRole = (survey: any, session: any): boolean => { return roleChecks[session.role as keyof typeof roleChecks]?.() ?? false } +/** + * @description 조사 매물 조회 + * @param {number} id 조사 매물 ID + * @returns {Promise} 조사 매물 데이터 + */ const fetchSurvey = async (id: number) => { // @ts-ignore return await prisma.SD_SURVEY_SALES_BASIC_INFO.findFirst({ @@ -55,7 +91,42 @@ const fetchSurvey = async (id: number) => { }) } -// API Route Handlers +/** + * @api {GET} /api/survey-sales/:id 조사 매물 조회 API + * @apiName GET /api/survey-sales/:id + * @apiGroup SurveySales + * @apiDescription 조사 매물 조회 API + * + * @apiParam {Number} id 조사 매물 PRIMARY KEY ID (required) + * @apiParam {Boolean} isPdf pdf 데이터 조회 여부 (optional, default: false) + * + * @apiSuccess {Object} SurveySaleBasicInfo 조사 매물 기본 정보 + * + * @apiError {Number} 401 세션 정보 없음 (로그인 필요) + * @apiError {Number} 403 권한 없음 + * @apiError {Number} 404 조사 매물 없음 + * @apiError {Number} 500 서버 오류 + * + * @apiExample {curl} Example usage: + * curl -X GET \ + * -G "isPdf=true" \ + * http://localhost:3000/api/survey-sales/1 + * + * @apiSuccessExample {json} Success-Response: + * { + * "id": 1, + * "srlNo": "1234567890", + * "storeId": "1234567890", + * "detailInfo": { + * "id": 1, + * "trestleMfpcCd": "1234567890", + * "trestleManufacturerProductName": "1234567890", + * "memo": "1234567890" + * ... + * } + * ... + * } + */ export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { const cookieStore = await cookies() @@ -69,14 +140,17 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: ERROR_MESSAGES.NOT_FOUND }, { status: 404 }) } + /** pdf 데이터 요청 여부, 권한 여부 확인 */ if (isPdf || checkRole(survey, session)) { return NextResponse.json(survey) } + /** 로그인 여부 확인 */ if (!session?.isLoggedIn || session?.role === null) { return NextResponse.json({ error: ERROR_MESSAGES.UNAUTHORIZED }, { status: 401 }) } + /** 권한 없음 */ return NextResponse.json({ error: ERROR_MESSAGES.NO_PERMISSION }, { status: 403 }) } catch (error) { console.error('Error fetching survey:', error) @@ -84,6 +158,13 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ } } +/** + * @description 새로운 SRL_NO 생성 + * @param {string} srlNo 기존 SRL_NO + * @param {string} storeId 판매점 ID + * @param {string} role 권한 + * @returns {Promise} 새로운 SRL_NO + */ const getNewSrlNo = async (srlNo: string, storeId: string, role: string) => { const srlRole = role === 'T01' || role === 'Admin' ? 'HO' : role === 'Admin_Sub' || role === 'Builder' ? 'HM' : '' @@ -113,6 +194,39 @@ const getNewSrlNo = async (srlNo: string, storeId: string, role: string) => { return newSrlNo } +/** + * @api {PUT} /api/survey-sales/:id 조사 매물 수정 API + * @apiName PUT /api/survey-sales/:id + * @apiGroup SurveySales + * @apiDescription 조사 매물 수정 API + * + * @apiParam {Number} id 조사 매물 PRIMARY KEY ID (required) + * @apiBody {Object} survey 조사 매물 데이터 (required) + * @apiBody {Boolean} isTemporary 임시 저장 여부 (optional, default: false) + * @apiBody {String} storeId 판매점 ID (optional) + * @apiBody {String} role 권한 (optional) + * + * @apiSuccess {Object} SurveySaleBasicInfo 수정된 조사 매물 기본 정보 + * + * @apiExample {curl} Example usage: + * curl -X PUT \ + * -H "Content-Type: application/json" \ + * -d '{"survey": {"detailInfo": {"id": 1, "memo": "1234567890", ...}, "srlNo": "1234567890", ...},"storeId": "1234567890", "role": "T01", "isTemporary": false}' \ + * http://localhost:3000/api/survey-sales/1 + * + * @apiSuccessExample {json} Success-Response: + * { + * "id": 1, + * "srlNo": "1234567890", + * "storeId": "1234567890", + * "detailInfo": { + * "id": 1, + * "memo": "1234567890", + * ... + * } + * ... + * } + * */ export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { const { id } = await params @@ -142,7 +256,24 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'データ保存に失敗しました。' }, { status: 500 }) } } - +/** + * @api {DELETE} /api/survey-sales/:id 조사 매물 삭제 API + * @apiName DELETE /api/survey-sales/:id + * @apiGroup SurveySales + * @apiDescription 조사 매물 삭제 API + * + * @apiParam {Number} id 조사 매물 PRIMARY KEY ID (required) + * + * @apiSuccess {String} message 삭제 성공 메시지 + * + * @apiExample {curl} Example usage: + * curl -X DELETE \ + * http://localhost:3000/api/survey-sales/1 + * + * @apiSuccessExample {json} Success-Response: + * { + * "message": "success" + */ export async function DELETE(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { const { id } = await params @@ -176,6 +307,41 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise } } +/** + * @api {PATCH} /api/survey-sales/:id 조사 매물 제출 API + * @apiName PATCH /api/survey-sales/:id + * @apiGroup SurveySales + * @apiDescription 조사 매물 제출 API + * + * @apiParam {Number} id 조사 매물 PRIMARY KEY ID (required) + * @apiBody {String} targetId 제출 대상 ID (required) + * @apiBody {String} targetNm 제출 대상 이름 (required) + * + * @apiSuccess {String} message 제출 성공 메시지 + * @apiSuccess {Object} data 제출된 조사 매물 기본 정보 + * + * @apiExample {curl} Example usage: + * curl -X PATCH \ + * -H "Content-Type: application/json" \ + * -d '{"targetId": "1234567890", "targetNm": "1234567890"}' \ + * http://localhost:3000/api/survey-sales/1 + * + * @apiSuccessExample {json} Success-Response: + * { + * "message": "success", + * "data": { + * "id": 1, + * "srlNo": "1234567890", + * "storeId": "1234567890", + * "detailInfo": { + * "id": 1, + * "memo": "1234567890", + * ... + * } + * ... + * } + * } + */ export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { try { const { id } = await params diff --git a/src/app/api/survey-sales/route.ts b/src/app/api/survey-sales/route.ts index dfb486c..c257b05 100644 --- a/src/app/api/survey-sales/route.ts +++ b/src/app/api/survey-sales/route.ts @@ -2,17 +2,17 @@ import { NextResponse } from 'next/server' import { prisma } from '@/libs/prisma' import { convertToSnakeCase } from '@/utils/common-utils' /** - * 검색 파라미터 + * @description 검색 파라미터 타입 */ type SearchParams = { - keyword?: string | null // 검색어 - searchOption?: string | null // 검색 옵션 (select 옵션) - isMySurvey?: string | null // 내가 작성한 매물 - sort?: string | null // 정렬 방식 + keyword?: string | null + searchOption?: string | null + isMySurvey?: string | null + sort?: string | null offset?: string | null - role?: string | null // 회원권한한 - storeId?: string | null // 판매점ID - builderId?: string | null // 시공ID + role?: string | null + storeId?: string | null + builderId?: string | null } type WhereCondition = { @@ -21,35 +21,35 @@ type WhereCondition = { [key: string]: any } -// 검색 가능한 필드 옵션 +/** 검색 가능한 필드 옵션 */ const SEARCH_OPTIONS = [ - 'BUILDING_NAME', // 건물명 - 'REPRESENTATIVE', // 담당자 - 'STORE', // 판매점명 - 'STORE_ID', // 판매점ID - 'CONSTRUCTION_POINT', // 시공점명 - 'CONSTRUCTION_POINT_ID', // 시공점ID - 'CUSTOMER_NAME', // 고객명 - 'POST_CODE', // 우편번호 - 'ADDRESS', // 주소 - 'ADDRESS_DETAIL', // 상세주소 - 'SRL_NO', // 등록번호 + 'BUILDING_NAME', + 'REPRESENTATIVE', + 'STORE', + 'STORE_ID', + 'CONSTRUCTION_POINT', + 'CONSTRUCTION_POINT_ID', + 'CUSTOMER_NAME', + 'POST_CODE', + 'ADDRESS', + 'ADDRESS_DETAIL', + 'SRL_NO', ] as const -// 페이지당 항목 수 +/** 페이지당 기본 항목 수 */ const ITEMS_PER_PAGE = 10 /** - * 키워드 검색 조건 생성 함수 - * @param keyword 검색 키워드 - * @param searchOption 검색 옵션 - * @returns 검색 조건 객체 + * @description 키워드 검색 조건 생성 함수 + * @param {string} keyword 검색 키워드 + * @param {string} searchOption 검색 옵션 + * @returns {WhereCondition} 검색 조건 객체 */ const createKeywordSearchCondition = (keyword: string, searchOption: string): WhereCondition => { const where: WhereCondition = { AND: [] } if (searchOption === 'all') { - // 모든 필드 검색 시 OR 조건 사용 + /** 모든 필드 검색 시 OR 조건 사용 */ where.OR = [] where.OR.push( @@ -58,42 +58,38 @@ const createKeywordSearchCondition = (keyword: string, searchOption: string): Wh })), ) } else if (SEARCH_OPTIONS.includes(searchOption.toUpperCase() as any)) { - // 특정 필드 검색 + /** 특정 필드 검색 */ where[searchOption.toUpperCase()] = { contains: keyword } } return where } /** - * 회원 역할별 검색 조건 생성 함수 - * @param params 검색 파라미터 - * @returns 검색 조건 객체 + * @description 회원 역할별 검색 조건 생성 함수 + * @param {SearchParams} params 검색 파라미터 + * @returns {WhereCondition} 검색 조건 객체 */ const createMemberRoleCondition = (params: SearchParams): WhereCondition => { const where: WhereCondition = { AND: [] } switch (params.role) { - case 'Admin': // 1차점 + case 'Admin': where.OR = [ { - // 같은 판매점에서 작성한 제출/제출되지 않은 매물 AND: [{ STORE_ID: { equals: params.storeId } }], }, { - // MUSUBI (시공권한 X) 가 ORDER 에 제출한 매물 AND: [{ SUBMISSION_TARGET_ID: { equals: params.storeId } }, { SUBMISSION_STATUS: { equals: true } }], }, ] break - case 'Admin_Sub': // 2차점 + case 'Admin_Sub': where.OR = [ { - // MUSUBI (시공권한 X) 같은 판매점에서 작성한 제출/제출되지 않은 매물 AND: [{ STORE_ID: { equals: params.storeId } }, { CONSTRUCTION_POINT_ID: { equals: params.builderId } }], }, { - // MUSUBI (시공권한 O) 가 MUSUBI 에 제출한 매물 + PARTNER 가 제출한 매물 AND: [ { SUBMISSION_TARGET_ID: { equals: params.storeId } }, { CONSTRUCTION_POINT_ID: { not: null } }, @@ -104,9 +100,8 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => { ] break - case 'Builder': // MUSUBI (시공권한 O) - case 'Partner': // PARTNER - // 시공ID 같은 매물 + case 'Builder': + case 'Partner': where.AND?.push({ CONSTRUCTION_POINT_ID: { equals: params.builderId }, }) @@ -129,12 +124,16 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => { ] break case 'User': - // 모든 매물 조회 가능 (추가 조건 없음) break } return where } +/** + * @description 권한 별 필수 값 존재 여부 확인, 없을 시 빈 데이터 반환 + * @param {SearchParams} params 검색 파라미터 + * @returns {NextResponse} 세션 체크 결과 + */ const checkSession = (params: SearchParams) => { if (params.role === null) { return NextResponse.json({ data: [], count: 0 }) @@ -152,11 +151,50 @@ const checkSession = (params: SearchParams) => { } /** - * GET 핸들러 - 설문 목록 조회 + * @api {GET} /api/survey-sales 설문 목록 조회 API + * @apiName GET /api/survey-sales + * @apiGroup SurveySales + * @apiDescription 설문 목록 조회 API + * + * @apiParam {String} keyword 검색어 (optional) + * @apiParam {String} searchOption 검색 옵션 (optional) + * @apiParam {String} isMySurvey 내가 작성한 매물 (optional) + * @apiParam {String} sort 정렬 방식 (optional) + * @apiParam {String} offset 페이지 오프셋 (optional) + * @apiParam {String} role 회원권한 (optional) + * @apiParam {String} storeId 판매점ID (optional) + * @apiParam {String} builderId 시공점ID (optional) + * + * @apiSuccess {Object[]} data 설문 목록 데이터 + * @apiSuccess {Number} data.count 설문 목록 개수 + * + * @apiExample {curl} Example usage: + * curl -X GET \ + * -G "keyword=test&searchOption=all&isMySurvey=true&sort=created&offset=0&role=Admin&storeId=1234567890&builderId=1234567890" \ + * http://localhost:3000/api/survey-sales + * + * @apiSuccessExample {json} Success-Response: + * { + * "data": [ + * { + * "id": 1, + * "srlNo": "1234567890", + * "storeId": "1234567890", + * "detailInfo": { + * "id": 1, + * "memo": "1234567890", + * ... + * } + * ... + * } + * ], + * "count": 1 + * } + * */ export async function GET(request: Request) { try { - // URL 파라미터 파싱 + /** URL 파라미터 파싱 */ const { searchParams } = new URL(request.url) const params: SearchParams = { keyword: searchParams.get('keyword'), @@ -169,31 +207,31 @@ export async function GET(request: Request) { builderId: searchParams.get('builderId'), } - // 세션 체크 결과 처리 + /** 세션 체크 결과 처리 */ const sessionCheckResult = checkSession(params) if (sessionCheckResult) { return sessionCheckResult } - // 검색 조건 구성 + /** 검색 조건 구성 */ const where: WhereCondition = { AND: [] } - // 내가 작성한 매물 조건 적용 + /** 내가 작성한 매물 조건 적용 */ if (params.isMySurvey) { where.AND.push({ REPRESENTATIVE_ID: params.isMySurvey }) } - // 키워드 검색 조건 적용 + /** 키워드 검색 조건 적용 */ if (params.keyword && params.searchOption) { where.AND.push(createKeywordSearchCondition(params.keyword, params.searchOption)) } - // 회원 유형 조건 적용 + /** 회원 유형 조건 적용 */ const roleCondition = createMemberRoleCondition(params) if (Object.keys(roleCondition).length > 0) { where.AND.push(roleCondition) } - // 페이지네이션 데이터 조회 + /** 페이지네이션 데이터 조회 */ //@ts-ignore const surveys = await prisma.SD_SURVEY_SALES_BASIC_INFO.findMany({ where, @@ -201,7 +239,7 @@ export async function GET(request: Request) { skip: Number(params.offset), take: ITEMS_PER_PAGE, }) - // 전체 개수만 조회 + /** 전체 개수만 조회 */ //@ts-ignore const count = await prisma.SD_SURVEY_SALES_BASIC_INFO.count({ where }) return NextResponse.json({ data: { data: surveys, count: count } }) @@ -212,19 +250,41 @@ export async function GET(request: Request) { } /** - * PUT 핸들러 - 상세 정보 추가 + * @api {PUT} /api/survey-sales 설문 상세 정보 추가 API + * @apiName PUT /api/survey-sales + * @apiGroup SurveySales + * @apiDescription 설문 상세 정보 추가 API + * + * @apiParam {Number} id 설문 목록 ID (required) + * @apiBody {Object} detail_info 상세 정보 (required) + * + * @apiSuccess {String} message 성공 메시지 + * + * @apiExample {curl} Example usage: + * curl -X PUT \ + * -H "Content-Type: application/json" \ + * -d '{"id": 1, "detail_info": {"memo": "1234567890"}}' \ + * http://localhost:3000/api/survey-sales + * + * @apiSuccessExample {json} Success-Response: + * { + * "message": "Success Update Survey" + * } + * + * @apiError {Number} 500 서버 오류 */ export async function PUT(request: Request) { try { + /** 요청 바디 파싱 */ const body = await request.json() - // 상세 정보 생성을 위한 데이터 구성 + /** 상세 정보 생성을 위한 데이터 구성 */ const detailInfo = { ...body.detail_info, BASIC_INFO_ID: body.id, } - // 상세 정보 생성 + /** 상세 정보 생성 */ //@ts-ignore await prisma.SD_SURVEY_SALES_DETAIL_INFO.create({ data: detailInfo, @@ -239,6 +299,42 @@ export async function PUT(request: Request) { } } +/** + * @api {POST} /api/survey-sales 설문 상세 정보 추가 API + * @apiName POST /api/survey-sales + * @apiGroup SurveySales + * @apiDescription 설문 상세 정보 추가 API + * + * @apiParam {Object} survey 설문 목록 데이터 (required) + * @apiParam {String} role 회원권한 (required) + * @apiParam {String} storeId 판매점ID (required) + * @returns + * + * @apiSuccess {Object} data 설문 목록 데이터 + * + * @apiExample {curl} Example usage: + * curl -X POST \ + * -H "Content-Type: application/json" \ + * -d '{"survey": {"srlNo": "1234567890", "storeId": "1234567890", "role": "T01", "detail_info": {"memo": "1234567890"}}}' \ + * http://localhost:3000/api/survey-sales + * + * @apiSuccessExample {json} Success-Response: + * { + * "data": { + * "id": 1, + * "srlNo": "1234567890", + * "storeId": "1234567890", + * "detailInfo": { + * "id": 1, + * "memo": "1234567890", + * ... + * } + * ... + * } + * } + * + * @apiError {Number} 500 서버 오류 + */ export async function POST(request: Request) { try { const body = await request.json() @@ -252,8 +348,8 @@ export async function POST(request: Request) { ? '' : null - // 임시 저장 시 임시저장으로 저장 - // 기본 저장 시 (HO/HM) + 판매점ID + yyMMdd + 000 으로 저장 + /** 임시 저장 시 임시저장으로 저장 */ + /** 기본 저장 시 (HO/HM) + 판매점ID + yyMMdd + 000 으로 저장 */ const baseSrlNo = body.survey.srlNo ?? role + @@ -274,10 +370,10 @@ export async function POST(request: Request) { }, }) - // 마지막 번호 추출 + /** 마지막 번호 추출 */ const lastNumber = lastSurvey ? parseInt(lastSurvey.SRL_NO.slice(-3)) : 0 - // 새로운 srlNo 생성 - 임시저장일 경우 '임시저장' 으로 저장 + /** 새로운 srlNo 생성 - 임시저장일 경우 '임시저장' 으로 저장 */ const newSrlNo = baseSrlNo.startsWith('一時保存') ? baseSrlNo : baseSrlNo + (lastNumber + 1).toString().padStart(3, '0') const { detailInfo, ...basicInfo } = body.survey diff --git a/src/components/inquiry/RegistForm.tsx b/src/components/inquiry/RegistForm.tsx index 93df62f..7ad869e 100644 --- a/src/components/inquiry/RegistForm.tsx +++ b/src/components/inquiry/RegistForm.tsx @@ -50,6 +50,7 @@ export default function RegistForm() { const [attachedFiles, setAttachedFiles] = useState([]) + /** 파일 첨부 처리 */ const handleFileChange = (e: React.ChangeEvent) => { const files = e.target.files if (files && files.length > 0) { @@ -58,15 +59,18 @@ export default function RegistForm() { e.target.value = '' } + /** 파일 삭제 처리 */ const handleRemoveFile = (index: number) => { setAttachedFiles(attachedFiles.filter((_, i) => i !== index)) } + /** 필수 필드 포커스 처리 */ const focusOnRequiredField = (fieldId: string) => { const element = document.getElementById(fieldId) if (element) element.focus() } + /** 제출 처리 */ const handleSubmit = async () => { const emptyField = requiredFieldNames.find((field) => inquiryRequest[field.id as keyof InquiryRequest] === '') if (emptyField) { diff --git a/src/components/inquiry/list/ListTable.tsx b/src/components/inquiry/list/ListTable.tsx index 14ffece..b1359f7 100644 --- a/src/components/inquiry/list/ListTable.tsx +++ b/src/components/inquiry/list/ListTable.tsx @@ -57,6 +57,7 @@ export default function ListTable() { } }, [session, inquiryList]) + /** 내 문의 필터 처리 - 체크 시 자신의 문의 목록만 조회 */ const handleMyInquiry = () => { setOffset(1) setInquiryListRequest({ @@ -65,6 +66,7 @@ export default function ListTable() { }) } + /** 답변 여부 필터 처리리 */ const handleFilter = (e: React.ChangeEvent) => { switch (e.target.value) { case 'N': diff --git a/src/components/pdf/SurveySaleDownloadPdf.tsx b/src/components/pdf/SurveySaleDownloadPdf.tsx index 0cda0e1..0030612 100644 --- a/src/components/pdf/SurveySaleDownloadPdf.tsx +++ b/src/components/pdf/SurveySaleDownloadPdf.tsx @@ -20,6 +20,7 @@ export default function SurveySaleDownloadPdf() { const targetRef = useRef(null) const isGeneratedRef = useRef(false) + /** 페이지 랜더링 이후 PDF 생성 */ useEffect(() => { if (isLoadingSurveyDetail || isGeneratedRef.current) return if (surveyDetail === null) { @@ -55,6 +56,7 @@ export default function SurveySaleDownloadPdf() { }, } + /** PDF 생성 이후 세션 여부에 따른 라우팅 처리 */ generatePDF(targetRef, options) .then(() => { setIsShow(false) @@ -75,8 +77,8 @@ export default function SurveySaleDownloadPdf() {
({ ...prev, [field]: value })) } + /** 필수값 검증 */ const validateData = (data: SubmitFormData): boolean => { const requiredFields = FORM_FIELDS.filter((field) => field.required) @@ -110,7 +111,7 @@ export default function SurveySaleSubmitPopup() { return true } - // TODO: Admin_Sub 계정 매핑된 submit target id 추가!!!! && 메일 테스트트 + /** 제출 처리 - 데이터 검증 이후 메일 전송 완료되면 데이터 저장 */ const handleSubmit = () => { if (validateData(submitData)) { window.neoConfirm('送信しますか? 送信後は変更・修正することはできません。', () => { @@ -145,6 +146,7 @@ export default function SurveySaleSubmitPopup() { popupController.setSurveySaleSubmitPopup(false) } + /** 권한 별 폼 필드 렌더링 */ const renderFormField = (field: FormField) => { const isReadOnly = false diff --git a/src/components/survey-sale/detail/BasicForm.tsx b/src/components/survey-sale/detail/BasicForm.tsx index 520393d..fe2b432 100644 --- a/src/components/survey-sale/detail/BasicForm.tsx +++ b/src/components/survey-sale/detail/BasicForm.tsx @@ -1,7 +1,6 @@ 'use client' import { useEffect, useState } from 'react' -import { useSurveySaleTabState } from '@/store/surveySaleTabState' import type { SurveyBasicRequest } from '@/types/Survey' import type { Mode } from 'fs' import { usePopupController } from '@/store/popupController' @@ -16,16 +15,10 @@ interface BasicFormProps { } export default function BasicForm({ basicInfo, setBasicInfo, mode, session }: BasicFormProps) { - const { setBasicInfoSelected } = useSurveySaleTabState() const [isFlip, setIsFlip] = useState(true) const { addressData, resetAddressData } = useAddressStore() const popupController = usePopupController() - useEffect(() => { - setBasicInfoSelected() - }, []) - - // 주소 데이터가 변경될 때만 업데이트 useEffect(() => { if (!addressData) return setBasicInfo({ @@ -59,6 +52,7 @@ export default function BasicForm({ basicInfo, setBasicInfo, mode, session }: Ba onChange={(e) => setBasicInfo({ ...basicInfo, representative: e.target.value })} />
+ {/* 페이지 모드 별, 권한 별 판매점, 시공점 입력 여부 처리 */} {mode === 'READ' || session?.role === 'Builder' ? ( <> {storeInput(basicInfo, setBasicInfo, mode)} @@ -148,6 +142,7 @@ export default function BasicForm({ basicInfo, setBasicInfo, mode, session }: Ba ) } +/** 판매점 입력 창 */ const storeInput = (basicInfo: SurveyBasicRequest, setBasicInfo: (basicInfo: SurveyBasicRequest) => void, mode: Mode) => { return (
@@ -163,6 +158,7 @@ const storeInput = (basicInfo: SurveyBasicRequest, setBasicInfo: (basicInfo: Sur ) } +/** 시공점 입력 창 */ const builderInput = (basicInfo: SurveyBasicRequest, setBasicInfo: (basicInfo: SurveyBasicRequest) => void, mode: Mode) => { return (
diff --git a/src/components/survey-sale/detail/ButtonForm.tsx b/src/components/survey-sale/detail/ButtonForm.tsx index d452aec..044a1d4 100644 --- a/src/components/survey-sale/detail/ButtonForm.tsx +++ b/src/components/survey-sale/detail/ButtonForm.tsx @@ -3,7 +3,7 @@ import type { Mode, SurveyBasicRequest, SurveyDetailInfo, SurveyDetailRequest } from '@/types/Survey' import { useSessionStore } from '@/store/session' import { useEffect, useState } from 'react' -import { useParams, useRouter, useSearchParams } from 'next/navigation' +import { useParams, useRouter } from 'next/navigation' import { requiredFields, useSurvey } from '@/hooks/useSurvey' import { usePopupController } from '@/store/popupController' @@ -62,6 +62,7 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { }) }, [session, data]) + /** 권한 정리 로직 - 작성자(담당자), 제출 권한자, 제출 수신자*/ const calculatePermissions = (session: any, basicData: SurveyBasicRequest): PermissionState => { const isSubmiter = calculateSubmitPermission(session, basicData) const isWriter = session.userId === basicData.representativeId @@ -70,6 +71,7 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { return { isSubmiter, isWriter, isReceiver } } + /** 제출 권한 체크 */ const calculateSubmitPermission = (session: any, basicData: SurveyBasicRequest): boolean => { switch (session?.role) { case 'T01': @@ -85,6 +87,7 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { } } + /** 저장 로직 */ const handleSave = (isTemporary: boolean, isSubmitProcess: boolean) => { const emptyField = validateSurveyDetail(data.roof) const hasEmptyField = emptyField?.trim() !== '' @@ -96,7 +99,9 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { } } + /** 임시 저장 로직 */ const tempSaveProcess = async () => { + /**route 에 id 가 있는 경우 업데이트, 없는 경우 생성 */ if (!Number.isNaN(id)) { await updateSurvey({ survey: saveData, isTemporary: true }) if (!isUpdatingSurvey) { @@ -115,11 +120,13 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { alert('一時保存されました。') } + /** 입력 필드 포커스 처리 */ const focusInput = (field: keyof SurveyDetailInfo) => { const input = document.getElementById(field) input?.focus() } + /** 저장 로직 */ const saveProcess = async (emptyField: string | null, isSubmitProcess?: boolean) => { if (emptyField?.trim() === '') { await handleSuccessfulSave(isSubmitProcess) @@ -128,7 +135,9 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { } } + /** 저장 성공 로직 */ const handleSuccessfulSave = async (isSubmitProcess?: boolean) => { + /** route 에 id 가 있는 경우 업데이트, 없는 경우 생성 */ if (!Number.isNaN(id)) { await updateSurvey({ survey: saveData, @@ -139,6 +148,7 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { setMode('READ') } } else { + /** 제출 로직인 경우 search param 추가 */ const savedId = await createSurvey(saveData) if (isSubmitProcess) { await router.push(`/survey-sale/${savedId}?show=true`) @@ -149,6 +159,7 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { } } + /** 필수값 미입력 처리 */ const handleFailedSave = (emptyField: string | null) => { if (emptyField?.includes('Unit')) { alert('電気契約容量の単位を入力してください。') @@ -158,6 +169,7 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { focusInput(emptyField as keyof SurveyDetailInfo) } + /** 삭제 로직 */ const handleDelete = async () => { if (!Number.isNaN(id)) { window.neoConfirm('削除しますか?', async () => { @@ -170,6 +182,7 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { } } + /** 제출 로직 */ const handleSubmit = async () => { if (data.basic.srlNo?.startsWith('一時保存') && Number.isNaN(id)) { alert('一時保存されたデータは提出できません。') @@ -187,8 +200,10 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { } } + /** 로그인 여부 체크 */ if (!session?.isLoggedIn) return null + /** 읽기 모드, 제출 된 데이터, 제출 권한자는 리스트 버튼만 표시 */ if (mode === 'READ' && isSubmit && permissions.isSubmiter) { return (
@@ -201,6 +216,10 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { return ( <> + {/* 읽기 모드 버튼 처리 */} + {/* 작성자 - 수정, 삭제, 제출(미제출인 매물) 버튼 표시 */} + {/* 제출권한자 - 수정, 제출(미제출인 매물) 버튼 표시 */} + {/* 제출수신자 - 수정, 삭제 버튼 표시 */} {mode === 'READ' && (
@@ -212,6 +231,10 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) {
)} + {/* 수정, 작성 모드 */} + {/* 작성자 - 임시저장, 저장, 제출(미제출인 매물) 버튼 표시 */} + {/* 제출권한자 - 임시저장, 저장, 제출(미제출인 매물) 버튼 표시 */} + {/* 제출수신자 - 임시저장, 저장 버튼 표시 */} {(mode === 'CREATE' || mode === 'EDIT') && (
diff --git a/src/components/survey-sale/detail/DataTable.tsx b/src/components/survey-sale/detail/DataTable.tsx index bfafecc..38ca7db 100644 --- a/src/components/survey-sale/detail/DataTable.tsx +++ b/src/components/survey-sale/detail/DataTable.tsx @@ -6,6 +6,7 @@ import { SurveyBasicInfo } from '@/types/Survey' export default function DataTable({ surveyDetail }: { surveyDetail: SurveyBasicInfo }) { const router = useRouter() + /** 제출 상태 처리 */ const submitStatus = () => { const { submissionTargetNm, submissionTargetId } = surveyDetail ?? {} diff --git a/src/components/survey-sale/detail/DetailForm.tsx b/src/components/survey-sale/detail/DetailForm.tsx index b4a995a..0f73c46 100644 --- a/src/components/survey-sale/detail/DetailForm.tsx +++ b/src/components/survey-sale/detail/DetailForm.tsx @@ -94,6 +94,7 @@ export default function DetailForm() { })) const [roofInfoData, setRoofInfoData] = useState(roofInfoForm) + /** 제출 팝업 처리 - createSurvey 이후 popup 처리 시 노드 삽입 오류로 인해 별도 처리 */ useEffect(() => { const show = searchParams.get('show') if (show === 'true') { @@ -102,7 +103,7 @@ export default function DetailForm() { } }, [searchParams, pathname]) - // 세션 데이터가 변경될 때 기본 정보 업데이트 + /** 세션 데이터가 변경될 때 기본 정보 업데이트 */ useEffect(() => { if (!session?.isLoggedIn) return setBasicInfoData((prev) => ({ @@ -116,6 +117,7 @@ export default function DetailForm() { })) }, [session?.isLoggedIn]) + /** 조사매물 상세 데이터 업데이트 */ useEffect(() => { if (!isLoadingSurveyDetail && surveyDetail && (mode === 'EDIT' || mode === 'READ')) { const { id, uptDt, regDt, detailInfo, ...rest } = surveyDetail diff --git a/src/components/survey-sale/detail/RoofForm.tsx b/src/components/survey-sale/detail/RoofForm.tsx index f30fc0a..e19d047 100644 --- a/src/components/survey-sale/detail/RoofForm.tsx +++ b/src/components/survey-sale/detail/RoofForm.tsx @@ -19,116 +19,145 @@ type SelectBoxKeys = | 'installationAvailability' export const supplementaryFacilities = [ - { id: 1, name: 'エコキュート' }, //에코큐트 - { id: 2, name: 'エネパーム' }, //에네팜 - { id: 3, name: '蓄電池システム' }, //축전지시스템 - { id: 4, name: '太陽光発電' }, //태양광발전 + /** 에코큐트 */ + { id: 1, name: 'エコキュート' }, + /** 에네팜 */ + { id: 2, name: 'エネパーム' }, + /** 축전지시스템 */ + { id: 3, name: '蓄電池システム' }, + /** 태양광발전 */ + { id: 4, name: '太陽光発電' }, ] export const roofMaterial = [ - { id: 1, name: 'スレート' }, //슬레이트 - { id: 2, name: 'アスファルトシングル' }, //아스팔트 싱글 - { id: 3, name: '瓦' }, //기와 - { id: 4, name: '金属屋根' }, //금속지붕 + /** 슬레이트 */ + { id: 1, name: 'スレート' }, + /** 아스팔트 싱글 */ + { id: 2, name: 'アスファルトシングル' }, + /** 기와 */ + { id: 3, name: '瓦' }, + /** 금속지붕 */ + { id: 4, name: '金属屋根' }, ] export const selectBoxOptions: Record = { installationSystem: [ { + /** 태양광발전 */ id: 1, - name: '太陽光発電', //태양광발전 + name: '太陽光発電', }, { + /** 하이브리드축전지시스템 */ id: 2, - name: 'ハイブリッド蓄電システム', //하이브리드축전지시스템 + name: 'ハイブリッド蓄電システム', }, { + /** 축전지시스템 */ id: 3, - name: '蓄電池システム', //축전지시스템 + name: '蓄電池システム', }, ], constructionYear: [ { + /** 신축 */ id: 1, - name: '新築', //신축 + name: '新築', }, { + /** 기축 */ id: 2, - name: '既築', //기존 + name: '既築', }, ], roofShape: [ { + /** 박공지붕 */ id: 1, - name: '切妻', //박공지붕 + name: '切妻', }, { + /** 기동 */ id: 2, - name: '寄棟', //기동 + name: '寄棟', }, { + /** 한쪽흐름 */ id: 3, - name: '片流れ', //한쪽흐름 + name: '片流れ', }, ], rafterSize: [ { + /** 35mm 이상×48mm 이상 */ id: 1, name: '幅35mm以上×高さ48mm以上', }, { + /** 36mm 이상×46mm 이상 */ id: 2, name: '幅36mm以上×高さ46mm以上', }, { + /** 37mm 이상×43mm 이상 */ id: 3, name: '幅37mm以上×高さ43mm以上', }, { + /** 38mm 이상×40mm 이상 */ id: 4, name: '幅38mm以上×高さ40mm以上', }, ], rafterPitch: [ { + /** 455mm 이하 */ id: 1, name: '455mm以下', }, { + /** 500mm 이하 */ id: 2, name: '500mm以下', }, { + /** 606mm 이하 */ id: 3, name: '606mm以下', }, ], openFieldPlateKind: [ { + /** 구조용합판 */ id: 1, - name: '構造用合板', //구조용합판 + name: '構造用合板', }, { + /** OSB */ id: 2, - name: 'OSB', //OSB + name: 'OSB', }, { + /** 파티클보드 */ id: 3, - name: 'パーティクルボード', //파티클보드 + name: 'パーティクルボード', }, { + /** 소판 */ id: 4, - name: '小幅板', //소판 + name: '小幅板', }, ], installationAvailability: [ { + /** 확인완료 */ id: 1, - name: '確認済み', //확인완료 + name: '確認済み', }, { + /** 미확인 */ id: 2, - name: '未確認', //미확인 + name: '未確認', }, ], } @@ -136,58 +165,69 @@ export const selectBoxOptions: Record = { structureOrder: [ { + /** 지붕재 - 방수재 - 지붕의기초 - 서까래 */ id: 1, - label: '屋根材 > 防水材 > 屋根の基礎 > 垂木', //지붕재 방수재 지붕의기초 서까래 + label: '屋根材 > 防水材 > 屋根の基礎 > 垂木', }, ], houseStructure: [ { + /** 목재 */ id: 1, label: '木製', }, ], rafterMaterial: [ { + /** 목재 */ id: 1, label: '木製', }, { + /** 강재 */ id: 2, label: '強制', }, ], waterproofMaterial: [ { + /** 아스팔트 지붕 940(22kg 이상) */ id: 1, label: 'アスファルト屋根940(22kg以上)', }, ], insulationPresence: [ { + /** 없음 */ id: 1, label: 'なし', }, { + /** 있음 */ id: 2, label: 'あり', }, ], rafterDirection: [ { + /** 수직 */ id: 1, label: '垂直垂木', }, { + /** 수평 */ id: 2, label: '水平垂木', }, ], leakTrace: [ { + /** 있음 */ id: 1, label: 'あり', }, { + /** 없음 */ id: 2, label: 'なし', }, @@ -210,6 +250,7 @@ export default function RoofForm(props: { const [isFlip, setIsFlip] = useState(true) const handleNumberInput = (key: keyof SurveyDetailRequest, value: number | string) => { + /** 지붕 경사도, 노지판 두께 처리 - 최대 5자리, 소수점 1자리 처리 */ if (key === 'roofSlope' || key === 'openFieldPlateThickness') { const stringValue = value.toString() if (stringValue.length > 5) { @@ -224,6 +265,7 @@ export default function RoofForm(props: { } } } + /** 전기 계약 용량 처리 - 단위 붙여서 저장*/ if (key === 'contractCapacity') { const remainValue = roofInfo.contractCapacity?.split(' ')[1] ?? roofInfo.contractCapacity if (Number.isNaN(Number(remainValue))) { @@ -235,6 +277,7 @@ export default function RoofForm(props: { setRoofInfo({ ...roofInfo, [key]: value.toString() }) } + /** 전기 계약 용량 단위 처리 */ const handleUnitInput = (value: string) => { const numericValue = roofInfo.contractCapacity?.replace(/[^0-9.]/g, '') || '' setRoofInfo({ @@ -461,6 +504,7 @@ export default function RoofForm(props: { ) } +/** SelectBox 처리 */ const SelectedBox = ({ mode, column, @@ -479,6 +523,7 @@ const SelectedBox = ({ const isSpecialCase = column === 'constructionYear' || column === 'installationAvailability' const showEtcOption = !isSpecialCase + /** SelectBox 값 변경 처리 */ const handleSelectChange = (e: React.ChangeEvent) => { const value = e.target.value const isEtc = value === 'etc' @@ -498,10 +543,16 @@ const SelectedBox = ({ setRoofInfo(updatedData) } + /** 기타 입력 처리 */ const handleEtcInputChange = (e: React.ChangeEvent) => { setRoofInfo({ ...detailInfoData, [`${column}Etc`]: e.target.value }) } + /** Input box 비활성화 처리 + * - 읽기 모드 : 비활성화 + * - 설치 가능 여부 : 기타 입력 창 항상 활성화 + * - 건축 연수 : 신축(1) 체크 시 비활성화 + * */ const isInputDisabled = () => { if (mode === 'READ') return true if (column === 'installationAvailability') return false @@ -552,6 +603,7 @@ const SelectedBox = ({ ) } +/** RadioBox 선택 처리 */ const RadioSelected = ({ mode, column, @@ -572,20 +624,26 @@ const RadioSelected = ({ const isSpecialColumn = column === 'rafterDirection' || column === 'leakTrace' || column === 'insulationPresence' const showEtcOption = !isSpecialColumn + /** RadioBox 값 변경 처리 */ const handleRadioChange = (e: React.ChangeEvent) => { const value = e.target.value + /** 누수 흔적 처리 - boolean 타입이므로 별도 처리 */ if (column === 'leakTrace') { setRoofInfo({ ...detailInfoData, leakTrace: value === '1' }) return } + /** 기타 체크 처리 */ if (value === 'etc') { setEtcChecked(true) setRoofInfo({ ...detailInfoData, [column]: null, [`${column}Etc`]: '' }) return } + /** 단열재 유무 - 있음(1) 선택 시 기타 체크 처리 + * 서까래 방향 - 기타 입력 칸 없음 + * */ const isInsulationPresence = column === 'insulationPresence' const isRafterDirection = column === 'rafterDirection' @@ -598,10 +656,15 @@ const RadioSelected = ({ }) } + /** 기타 입력 처리 */ const handleEtcInputChange = (e: React.ChangeEvent) => { setRoofInfo({ ...detailInfoData, [`${column}Etc`]: e.target.value }) } + /** Input box 비활성화 처리 + * - 읽기 모드 : 비활성화 + * - 단열재 유무 : 단열재 없음(1) 체크 시 비활성화 + * */ const isInputDisabled = () => { if (mode === 'READ') return true if (column === 'insulationPresence') { @@ -657,6 +720,7 @@ const RadioSelected = ({ ) } +/** 다중 선택 처리 */ const MultiCheck = ({ mode, column, @@ -675,6 +739,7 @@ const MultiCheck = ({ const isRoofMaterial = column === 'roofMaterial' const selectedValues = makeNumArr(String(roofInfo[column as keyof SurveyDetailInfo] ?? '')) + /** 다중 선택 처리 */ const handleCheckbox = (id: number) => { const isOtherSelected = Boolean(etcValue) let newValue: string[] @@ -682,6 +747,7 @@ const MultiCheck = ({ if (selectedValues.includes(String(id))) { newValue = selectedValues.filter((v) => v !== String(id)) } else { + /** 지붕 재료 처리 - 최대 2개 선택 처리 */ if (isRoofMaterial) { const totalSelected = selectedValues.length + (isOtherSelected || isOtherCheck ? 1 : 0) if (totalSelected >= 2) { @@ -694,6 +760,7 @@ const MultiCheck = ({ setRoofInfo({ ...roofInfo, [column]: newValue.join(',') }) } + /** 기타 선택 처리 */ const handleOtherCheckbox = () => { if (isRoofMaterial) { const currentSelected = selectedValues.length @@ -706,17 +773,19 @@ const MultiCheck = ({ const newIsOtherCheck = !isOtherCheck setIsOtherCheck(newIsOtherCheck) - // 기타 선택 해제 시 값도 null로 설정 + /** 기타 선택 해제 시 값도 null로 설정 */ setRoofInfo({ ...roofInfo, [`${column}Etc`]: newIsOtherCheck ? '' : null, }) } + /** 기타 입력 처리 */ const handleOtherInputChange = (e: React.ChangeEvent) => { setRoofInfo({ ...roofInfo, [`${column}Etc`]: e.target.value }) } + /** Input box 비활성화 처리 */ const isInputDisabled = () => { return mode === 'READ' || (!isOtherCheck && !etcValue) } diff --git a/src/components/survey-sale/list/SearchForm.tsx b/src/components/survey-sale/list/SearchForm.tsx index 3b910ee..83943a9 100644 --- a/src/components/survey-sale/list/SearchForm.tsx +++ b/src/components/survey-sale/list/SearchForm.tsx @@ -19,6 +19,7 @@ export default function SearchForm({ memberRole, userId }: { memberRole: string; setKeyword(searchKeyword) setSearchOption(option) } + /** 권한 별 검색 옵션 목록 처리 */ const searchOptions = memberRole === 'Partner' ? SEARCH_OPTIONS_PARTNERS : SEARCH_OPTIONS return ( diff --git a/src/hooks/useInquiry.ts b/src/hooks/useInquiry.ts index 7bec432..cd32c87 100644 --- a/src/hooks/useInquiry.ts +++ b/src/hooks/useInquiry.ts @@ -6,6 +6,21 @@ import { useMemo } from 'react' import { useSessionStore } from '@/store/session' import { useRouter } from 'next/navigation' +/** + * @description 문의사항 관련 기능을 제공하는 커스텀 훅 + * + * @param {number} [qnoNo] 문의사항 번호 + * @param {string} [compCd] 회사 코드 + * @returns {Object} 문의사항 관련 기능과 데이터 + * @returns {InquiryList[]} inquiryList - 문의사항 목록 + * @returns {boolean} isLoadingInquiryList - 문의사항 목록 로딩 상태 + * @returns {Inquiry|null} inquiryDetail - 문의사항 상세 정보 + * @returns {boolean} isLoadingInquiryDetail - 문의사항 상세 정보 로딩 상태 + * @returns {boolean} isSavingInquiry - 문의사항 저장 중 상태 + * @returns {Function} saveInquiry - 문의사항 저장 함수 + * @returns {Function} downloadFile - 파일 다운로드 함수 + * @returns {CommonCode[]} commonCodeList - 공통 코드 목록 + */ export function useInquiry( qnoNo?: number, compCd?: string, @@ -25,6 +40,12 @@ export function useInquiry( const { axiosInstance } = useAxios() const router = useRouter() + /** + * @description API 에러 처리 및 라우팅 + * + * @param {any} error 에러 객체 + * @returns {void} 라우팅 처리 + */ const errorRouter = (error: any) => { const status = error.response?.status alert(error.response?.data.error) @@ -50,6 +71,13 @@ export function useInquiry( } } + /** + * @description 문의사항 목록 조회 + * + * @returns {Object} 문의사항 목록 데이터 + * @returns {InquiryList[]} data - 문의사항 목록 + * @returns {boolean} isLoading - 문의사항 목록 로딩 상태 + */ const { data: inquiryList, isLoading: isLoadingInquiryList } = useQuery({ queryKey: ['inquiryList', inquiryListRequest, offset], queryFn: async () => { @@ -66,6 +94,12 @@ export function useInquiry( enabled: !!inquiryListRequest, }) + /** + * @description 문의사항 목록 데이터 메모이제이션 + * + * @returns {Object} 메모이제이션된 문의사항 목록 데이터 + * @returns {InquiryList[]} inquiryList - 문의사항 목록 + */ const inquriyListData = useMemo(() => { if (isLoadingInquiryList) { return { inquiryList: [] } @@ -75,6 +109,13 @@ export function useInquiry( } }, [inquiryList, isLoadingInquiryList]) + /** + * @description 문의사항 상세 정보 조회 + * + * @returns {Object} 문의사항 상세 정보 데이터 + * @returns {Inquiry|null} data - 문의사항 상세 정보 + * @returns {boolean} isLoading - 문의사항 상세 정보 로딩 상태 + */ const { data: inquiryDetail, isLoading: isLoadingInquiryDetail } = useQuery({ queryKey: ['inquiryDetail', qnoNo, compCd, session?.userId], queryFn: async () => { @@ -91,6 +132,12 @@ export function useInquiry( enabled: qnoNo !== undefined && compCd !== undefined, }) + /** + * @description 문의사항 저장 + * + * @param {FormData} formData 저장할 문의사항 데이터 + * @returns {Promise} 저장된 문의사항 응답 데이터 + */ const { mutateAsync: saveInquiry, isPending: isSavingInquiry } = useMutation({ mutationFn: async (formData: FormData) => { const resp = await axiosInstance(null).post<{ data: InquirySaveResponse }>('/api/qna/save', formData) @@ -104,6 +151,13 @@ export function useInquiry( }, }) + /** + * @description 파일 다운로드 + * + * @param {number} encodeFileNo 인코딩된 파일 번호 + * @param {string} srcFileNm 원본 파일명 + * @returns {Promise} 다운로드된 파일 데이터 또는 null + */ const downloadFile = async (encodeFileNo: number, srcFileNm: string) => { try { const resp = await fetch(`/api/qna/file?encodeFileNo=${encodeFileNo}&srcFileNm=${srcFileNm}`) @@ -125,6 +179,13 @@ export function useInquiry( } } + /** + * @description 공통 코드 목록 조회 + * + * @returns {Object} 공통 코드 목록 데이터 + * @returns {CommonCode[]} data - 공통 코드 목록 + * @returns {boolean} isLoading - 공통 코드 목록 로딩 상태 + */ const { data: commonCodeList, isLoading: isLoadingCommonCodeList } = useQuery({ queryKey: ['commonCodeList'], queryFn: async () => { diff --git a/src/hooks/useSurvey.ts b/src/hooks/useSurvey.ts index bafe8ab..5e50e30 100644 --- a/src/hooks/useSurvey.ts +++ b/src/hooks/useSurvey.ts @@ -6,7 +6,6 @@ import { useSessionStore } from '@/store/session' import { useAxios } from './useAxios' import { queryStringFormatter } from '@/utils/common-utils' import { useRouter } from 'next/navigation' -import { usePopupController } from '@/store/popupController' export const requiredFields = [ { @@ -55,7 +54,24 @@ type ZipCode = { kana2: string kana3: string } - +/** + * @description 조사 매물 관련 기능을 제공하는 커스텀 훅 + * + * @param {number} [id] 조사 매물 ID + * @param {boolean} [isPdf] PDF 뷰 여부 + * @returns {Object} 조사 매물 관련 기능과 데이터 + * @returns {SurveyBasicInfo[]} surveyList - 조사 매물 목록 데이터 + * @returns {SurveyBasicInfo} surveyDetail - 조사 매물 상세 데이터 + * @returns {boolean} isLoadingSurveyList - 조사 매물 목록 로딩 상태 + * @returns {boolean} isLoadingSurveyDetail - 조사 매물 상세 데이터 로딩 상태 + * @returns {boolean} isCreatingSurvey - 조사 매물 생성 중 상태 + * @returns {boolean} isUpdatingSurvey - 조사 매물 수정 중 상태 + * @returns {boolean} isDeletingSurvey - 조사 매물 삭제 중 상태 + * @returns {boolean} isSubmittingSurvey - 조사 매물 제출 중 상태 + * @returns {Function} createSurvey - 조사 매물 생성 함수 + * @returns {Function} updateSurvey - 조사 매물 수정 함수 + * @returns {Function} deleteSurvey - 조사 매물 삭제 함수 + */ export function useSurvey( id?: number, isPdf?: boolean, @@ -84,6 +100,12 @@ export function useSurvey( const { axiosInstance } = useAxios() const router = useRouter() + /** + * @description 조사 매물 목록, 상세 데이터 조회 에러 처리 + * + * @param {any} error 에러 객체 + * @returns {void} 라우팅 처리 + */ const errorRouter = (error: any) => { const status = error.response?.status alert(error.response?.data.error) @@ -109,6 +131,15 @@ export function useSurvey( } } + /** + * @description 조사 매물 목록 조회 + * + * @returns {Object} 조사 매물 목록 데이터 + * @returns {SurveyBasicInfo[]} 조사 매물 목록 데이터 + * @returns {number} 조건에 맞는 조사 매물 총 개수 + * @returns {() => void} 조사 매물 목록 데이터 새로고침 함수 + * @returns {boolean} 조사 매물 목록 로딩 상태 + */ const { data: surveyListData, isLoading: isLoadingSurveyList, @@ -136,6 +167,14 @@ export function useSurvey( } }, }) + + /** + * @description 조사 매물 목록 데이터 메모이제이션 + * + * @returns {Object} 메모이제이션된 조사 매물 목록 데이터 + * @returns {number} count - 조건에 맞는 조사 매물 총 개수 + * @returns {SurveyBasicInfo[]} data - 조사 매물 목록 데이터 + */ const surveyData = useMemo(() => { if (!surveyListData) return { count: 0, data: [] } return { @@ -143,6 +182,14 @@ export function useSurvey( } }, [surveyListData]) + /** + * @description 조사 매물 상세 데이터 조회 + * + * @returns {Object} 조사 매물 상세 데이터 + * @returns {SurveyBasicInfo} surveyDetail - 조사 매물 상세 데이터 + * @returns {boolean} isLoadingSurveyDetail - 조사 매물 상세 데이터 로딩 상태 + * @returns {() => void} refetchSurveyDetail - 조사 매물 상세 데이터 새로고침 함수 + */ const { data: surveyDetail, isLoading: isLoadingSurveyDetail, @@ -166,6 +213,12 @@ export function useSurvey( enabled: id !== 0 && id !== undefined && id !== null, }) + /** + * @description 조사 매물 생성 + * + * @param {SurveyRegistRequest} survey 생성할 조사 매물 데이터 + * @returns {Promise} 생성된 조사 매물 ID + */ const { mutateAsync: createSurvey, isPending: isCreatingSurvey } = useMutation({ mutationFn: async (survey: SurveyRegistRequest) => { const resp = await axiosInstance(null).post<{ id: number }>('/api/survey-sales', { @@ -181,6 +234,16 @@ export function useSurvey( }, }) + /** + * @description 조사 매물 수정 + * + * @param {Object} params 수정할 데이터 + * @param {SurveyRegistRequest} params.survey 수정할 조사 매물 데이터 + * @param {boolean} params.isTemporary 임시 저장 여부 + * @param {string|null} [params.storeId] 판매점 ID + * @returns {Promise} 수정된 조사 매물 데이터 + * @throws {Error} id가 없는 경우 에러 발생 + */ const { mutate: updateSurvey, isPending: isUpdatingSurvey } = useMutation({ mutationFn: async ({ survey, isTemporary, storeId }: { survey: SurveyRegistRequest; isTemporary: boolean; storeId?: string | null }) => { if (id === undefined) throw new Error('id is required') @@ -198,6 +261,16 @@ export function useSurvey( }, }) + /** + * @description 조사 매물 삭제 + * + * @returns {Promise} 삭제 성공 여부 + * @throws {Error} id가 없는 경우 에러 발생 + * + * @example + * // 삭제 성공 시 목록 데이터만 갱신하고, 상세 데이터는 갱신하지 않음 + * // 상세 데이터를 갱신하면 404 에러가 발생할 수 있음 + */ const { mutateAsync: deleteSurvey, isPending: isDeletingSurvey } = useMutation({ mutationFn: async () => { if (id === null) throw new Error('id is required') @@ -212,6 +285,15 @@ export function useSurvey( }, }) + /** + * @description 조사 매물 제출 + * + * @param {Object} params 제출할 데이터 + * @param {string|null} [params.targetId] 제출 대상 ID + * @param {string|null} [params.targetNm] 제출 대상 이름 + * @returns {Promise} 제출 성공 여부 + * @throws {Error} id가 없는 경우 에러 발생 + */ const { mutateAsync: submitSurvey, isPending: isSubmittingSurvey } = useMutation({ mutationFn: async ({ targetId, targetNm }: { targetId?: string | null; targetNm?: string | null }) => { if (!id) throw new Error('id is required') @@ -230,6 +312,12 @@ export function useSurvey( }, }) + /** + * @description 조사 매물 상세 데이터 유효성 검사 + * + * @param {SurveyDetailRequest} surveyDetail 검사할 조사 매물 상세 데이터 + * @returns {string} 빈 필드 이름 또는 빈 문자열 + */ const validateSurveyDetail = (surveyDetail: SurveyDetailRequest) => { const ETC_FIELDS = ['installationSystem', 'rafterSize', 'rafterPitch', 'waterproofMaterial', 'structureOrder'] as const @@ -269,6 +357,13 @@ export function useSurvey( return '' } + /** + * @description 우편번호 검색 + * + * @param {string} zipCode 검색할 우편번호 + * @returns {Promise} 우편번호 검색 결과 + * @throws {Error} 우편번호 검색 실패 시 에러 발생 + */ const getZipCode = async (zipCode: string): Promise => { try { const { data } = await axiosInstance(null).get( @@ -281,6 +376,14 @@ export function useSurvey( } } + /** + * @description 제출 대상 조회 + * + * @param {Object} params 조회할 데이터 + * @param {string} params.storeId 판매점 ID + * @param {string} params.role 사용자 권한 + * @returns {Promise} 제출 대상 목록 + */ const getSubmitTarget = async (params: { storeId: string; role: string }): Promise => { try { if (!params.storeId) { diff --git a/src/store/inquiryFilterStore.ts b/src/store/inquiryFilterStore.ts index b3fb8d9..6a3f676 100644 --- a/src/store/inquiryFilterStore.ts +++ b/src/store/inquiryFilterStore.ts @@ -1,6 +1,9 @@ import { InquiryListRequest } from '@/types/Inquiry' import { create } from 'zustand' +/** + * @description 문의 목록 필터 상태 타입 + */ type InquiryFilterState = { inquiryListRequest: InquiryListRequest setInquiryListRequest: (inquiryListRequest: InquiryListRequest) => void @@ -9,6 +12,15 @@ type InquiryFilterState = { setOffset: (offset: number) => void } +/** + * @description 문의 목록 필터 상태 관리 + * + * @param {InquiryListRequest} inquiryListRequest 문의 목록 요청 파라미터 + * @param {Function} setInquiryListRequest 문의 목록 요청 파라미터 설정 함수 + * @param {Function} reset 문의 목록 요청 파라미터 초기화 함수 + * @param {number} offset 문의 목록 페이지 오프셋 + * @param {Function} setOffset 문의 목록 페이지 오프셋 설정 함수 + */ export const useInquiryFilterStore = create((set) => ({ inquiryListRequest: { compCd: '5200', diff --git a/src/store/surveyFilterStore.ts b/src/store/surveyFilterStore.ts index 1442ad3..1f7860c 100644 --- a/src/store/surveyFilterStore.ts +++ b/src/store/surveyFilterStore.ts @@ -1,5 +1,11 @@ import { create } from 'zustand' +/** + * @description 조사 매물 검색 옵션 목록 + * + * @param {string} id 조사 매물 검색 옵션 ID + * @param {string} label 조사 매물 검색 옵션 라벨 + */ export const SEARCH_OPTIONS = [ { id: 'all', @@ -35,6 +41,12 @@ export const SEARCH_OPTIONS = [ }, ] +/** + * @description 조사 매물 검색 옵션 목록 - 파트너 + * + * @param {string} id 조사 매물 검색 옵션 ID + * @param {string} label 조사 매물 검색 옵션 라벨 + */ export const SEARCH_OPTIONS_PARTNERS = [ { id: 'all', @@ -54,8 +66,19 @@ export const SEARCH_OPTIONS_PARTNERS = [ }, ] +/** + * @description 조사 매물 검색 옵션 목록 타입 정의 + */ export type SEARCH_OPTIONS_ENUM = (typeof SEARCH_OPTIONS)[number]['id'] + +/** + * @description 파트너 권한의 조사 매물 검색 옵션 목록 타입 정의 + */ export type SEARCH_OPTIONS_PARTNERS_ENUM = (typeof SEARCH_OPTIONS_PARTNERS)[number]['id'] + +/** + * @description 조사 매물 정렬 옵션 목록 + */ export type SORT_OPTIONS_ENUM = 'created' | 'updated' type SurveyFilterState = { @@ -72,6 +95,22 @@ type SurveyFilterState = { reset: () => void } +/** + * @description 조사 매물 검색 조건 관리 + * + * @param {string} keyword 검색어 + * @param {SEARCH_OPTIONS_ENUM | SEARCH_OPTIONS_PARTNERS_ENUM} searchOption 검색 옵션 + * @param {string | null} isMySurvey 내 조사 매물 여부 + * @param {SORT_OPTIONS_ENUM} sort 정렬 옵션 + * @param {number} offset 페이지 오프셋 + * + * @param {Function} setKeyword 검색어 설정 함수 + * @param {Function} setSearchOption 검색 옵션 설정 함수 + * @param {Function} setIsMySurvey 내 조사 매물 여부 설정 함수 + * @param {Function} setSort 정렬 옵션 설정 함수 + * @param {Function} setOffset 페이지 오프셋 설정 함수 + * @param {Function} reset 필터 초기화 함수 + */ export const useSurveyFilterStore = create((set) => ({ keyword: '', searchOption: 'all', diff --git a/src/store/surveySaleTabState.ts b/src/store/surveySaleTabState.ts deleted file mode 100644 index f0ab766..0000000 --- a/src/store/surveySaleTabState.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { create } from 'zustand' - -type SurveySaleTabState = { - basicInfoSelected: boolean - roofInfoSelected: boolean - setBasicInfoSelected: () => void - setRoofInfoSelected: () => void - reset: () => void -} - -type InitialState = { - basicInfoSelected: boolean - roofInfoSelected: boolean -} - -const initialState: InitialState = { - basicInfoSelected: true, - roofInfoSelected: false, -} - -export const useSurveySaleTabState = create((set) => ({ - ...initialState, - setBasicInfoSelected: () => set((state) => ({ ...state, basicInfoSelected: true, roofInfoSelected: false })), - setRoofInfoSelected: () => set((state) => ({ ...state, basicInfoSelected: false, roofInfoSelected: true })), - reset: () => set(initialState), -})) diff --git a/src/types/Inquiry.ts b/src/types/Inquiry.ts index 98a98c3..646cce7 100644 --- a/src/types/Inquiry.ts +++ b/src/types/Inquiry.ts @@ -1,94 +1,190 @@ +/** + * @description 문의 목록 요청 파라미터 타입 + */ export type InquiryListRequest = { - compCd: string //company code - langCd: string //language code - storeId: string //store id - siteTpCd: string //site type code (QC: QCast, QR: QRead) - schTitle: string | null //search title - schRegId: string | null //search regId - schFromDt: string | null //search start date - schToDt: string | null //search end date - schAnswerYn: string | null //search answer yn - loginId: string //login id + /* 회사 코드 */ + compCd: string + /* 언어 코드 */ + langCd: string + /* 판매점 ID */ + storeId: string + /* 사이트 유형 코드 */ + siteTpCd: string + /* 검색 제목 */ + schTitle: string | null + /* 검색 등록자 ID */ + schRegId: string | null + /* 검색 시작 일자 */ + schFromDt: string | null + /* 검색 종료 일자 */ + schToDt: string | null + /* 검색 답변 여부 */ + schAnswerYn: string | null + /* 로그인 ID */ + loginId: string } +/** + * @description 문의 목록 응답 타입 + */ export type InquiryList = { - totCnt: number //total count - rowNumber: number //row number - compCd: string //company code - qnaNo: number //qna number - qstTitle: string //title - regDt: string //registration date - regId: string //registration Userid - regNm: string //registration User name - answerYn: string //answer yn - Y / N - attachYn: string | null //attach yn - Y / N - qnaClsLrgCd: string //qna CLS large Code - qnaClsMidCd: string //qna CLS Mid Code - qnaClsSmlCd: string | null //qna CLS Small Code - regUserNm: string //registration User name + /* 총 건수 */ + totCnt: number + /* 행 번호 */ + rowNumber: number + /* 회사 코드 */ + compCd: string + /* 문의 번호 */ + qnaNo: number + /* 문의 제목 */ + qstTitle: string + /* 문의 등록 일자 */ + regDt: string + /* 문의 등록자 이메일 */ + regEmail: string + /* 문의 등록자 ID */ + regId: string + /* 문의 등록자 이름 */ + regNm: string + /* 답변 여부 */ + answerYn: string + /* 첨부 여부 */ + attachYn: string | null + /* 문의 대분류 코드 */ + qnaClsLrgCd: string + /* 문의 중분류 코드 */ + qnaClsMidCd: string + /* 문의 소분류 코드 */ + qnaClsSmlCd: string | null + /* 문의 등록자 이름 */ + regUserNm: string } +/** + * @description 문의 상세 조회 요청 파라미터 타입 + */ export type InquiryDetailRequest = { - compCd: string //company code - langCd: string //language code - qnaNo: number //qna number - loginId: string //login id + /* 회사 코드 */ + compCd: string + /* 언어 코드 */ + langCd: string + /* 문의 번호 */ + qnaNo: number + /* 로그인 ID */ + loginId: string } +/** + * @description 문의 상세 조회 응답 타입 + */ export type Inquiry = { - compCd: string //company code - qnaNo: number //qna number - qstTitle: string //title - qstContents: string //content - regDt: string //registration date - regId: string //registration Userid - regNm: string //registration User name - regEmail: string //registration User email - answerYn: string //answer yn - Y / N - ansContents: string | null //answer content - ansRegDt: string | null //answer registration date - ansRegNm: string | null //answer registration User name - storeId: string | null //store id - storeNm: string | null //store name - regUserNm: string //registration User name - regUserTelNo: string | null //registration User tel number - qnaClsLrgCd: string //qna CLS large Code - qnaClsMidCd: string //qna CLS Mid Code - qnaClsSmlCd: string | null //qna CLS Small Code - listFile: listFile[] | null //Question list file - ansListFile: listFile[] | null //Answer list file + /* 회사 코드 */ + compCd: string + /* 문의 번호 */ + qnaNo: number + /* 문의 제목 */ + qstTitle: string + /* 문의 내용 */ + qstContents: string + /* 문의 등록 일자 */ + regDt: string + /* 문의 등록자 ID */ + regId: string + /* 문의 등록자 이름 */ + regNm: string + /* 문의 등록자 이메일 */ + regEmail: string + /* 답변 여부 */ + answerYn: string + /* 답변 내용 */ + ansContents: string | null + /* 답변 등록 일자 */ + ansRegDt: string | null + /* 답변 등록자 이름 */ + ansRegNm: string | null + /* 판매점 ID */ + storeId: string | null + /* 판매점 이름 */ + storeNm: string | null + /* 문의 등록자 이름 */ + regUserNm: string + /* 문의 등록자 전화번호 */ + regUserTelNo: string | null + /* 문의 대분류 코드 */ + qnaClsLrgCd: string + /* 문의 중분류 코드 */ + qnaClsMidCd: string + /* 문의 소분류 코드 */ + qnaClsSmlCd: string | null + /* 문의 첨부 파일 */ + listFile: listFile[] | null + /* 답변 첨부 파일 */ + ansListFile: listFile[] | null } +/** + * @description 문의 첨부 파일 타입 + */ export type listFile = { - fileNo: number //file number - encodeFileNo: string //encode file number - srcFileNm: string //source file name - fileCours: string //file course - fileSize: number //file size(Byte) - regDt: string //registration date + /* 파일 번호 */ + fileNo: number + /* 인코딩 파일 번호 */ + encodeFileNo: string + /* 소스 파일 이름 */ + srcFileNm: string + /* 파일 코스 */ + fileCours: string + /* 파일 크기 */ + fileSize: number + /* 등록 일자 */ + regDt: string } +/** + * @description 문의 등록 요청 파라미터 타입 + */ export type InquiryRequest = { - compCd: string //company code - siteTpCd: string //site type code(QC: QCast, QR: QRead) - qnaClsLrgCd: string //qna CLS large Code - qnaClsMidCd: string //qna CLS Mid Code - qnaClsSmlCd: string | null //qna CLS Small Code - title: string //title - contents: string //contents - regId: string //registration Userid - storeId: string //store id - regUserNm: string //registration User name - regUserTelNo: string | null //registration User tel number - qstMail: string //mail + /* 회사 코드 */ + compCd: string + /* 사이트 유형 코드 */ + siteTpCd: string + /* 문의 대분류 코드 */ + qnaClsLrgCd: string + /* 문의 중분류 코드 */ + qnaClsMidCd: string + /* 문의 소분류 코드 */ + qnaClsSmlCd: string | null + /* 문의 제목 */ + title: string + /* 문의 내용 */ + contents: string + /* 문의 등록자 ID */ + regId: string + /* 판매점 ID */ + storeId: string + /* 문의 등록자 이름 */ + regUserNm: string + /* 문의 등록자 전화번호 */ + regUserTelNo: string | null + /* 문의 이메일 */ + qstMail: string } +/** + * @description 문의 등록 응답 타입 + */ export type InquirySaveResponse = { - cnt: number | null //count - qnaNo: number //qna number - mailYn: string //mail yn - Y / N + /* 건수 */ + cnt: number | null + /* 문의 번호 */ + qnaNo: number + /* 메일 여부 */ + mailYn: string } +/** + * @description 공통 코드 타입 + */ export type CommonCode = { headCd: string code: string diff --git a/src/types/Survey.ts b/src/types/Survey.ts index 312c96b..d46dae7 100644 --- a/src/types/Survey.ts +++ b/src/types/Survey.ts @@ -1,156 +1,303 @@ +/** + * @description 조사 매물 타입 + */ export type SurveyBasicInfo = { + /* 조사 매물 ID */ id: number + /* 담당자명 */ representative: string + /* 담당자 ID */ representativeId: string | null + /* 판매점명 */ store: string | null + /* 판매점 ID */ storeId: string | null + /* 시공점명 */ constructionPoint: string | null + /* 시공점 ID */ constructionPointId: string | null + /* 조사 일자 */ investigationDate: string | null + /* 건물 이름 */ buildingName: string | null + /* 고객명 */ customerName: string | null + /* 우편번호 */ postCode: string | null + /* 주소 (도도부현) */ address: string | null + /* 상세 주소 */ addressDetail: string | null + /* 제출 상태 */ submissionStatus: boolean + /* 제출 일시 */ submissionDate: string | null + /* 조사 매물 상세 데이터 */ detailInfo: SurveyDetailInfo | null + /* 등록 일시 */ regDt: Date + /* 수정 일시 */ uptDt: Date + /* 제출 대상 판매점 ID */ submissionTargetId: string | null + /* 제출 대상 판매점명 */ submissionTargetNm: string | null - srlNo: string | null //판매점IDyyMMdd000 + /* 일련번호 */ + srlNo: string | null } +/** + * @description 조사 매물 상세 타입 + */ export type SurveyDetailInfo = { + /* 조사 매물 상세 ID */ id: number + /* 조사 매물 기본 데이터 ID */ basicInfoId: number + /* 전기계약 용량 */ contractCapacity: string | null + /* 전기 소매 회사 */ retailCompany: string | null + /* 전기 부대 설비 */ supplementaryFacilities: string | null // number 배열 + /* 전기 부대 설비 기타 */ supplementaryFacilitiesEtc: string | null + /* 설치 희망 시스템 */ installationSystem: string | null + /* 설치 희망 시스템 기타 */ installationSystemEtc: string | null + /* 건축 년도 */ constructionYear: string | null + /* 건축 년도 기타 */ constructionYearEtc: string | null - roofMaterial: string | null // number 배열 + /* 지붕재 - 다중 선택 가능 [number]*/ + roofMaterial: string | null + /* 지붕재 기타 */ roofMaterialEtc: string | null + /* 지붕모양 */ roofShape: string | null + /* 지붕모양 기타 */ roofShapeEtc: string | null + /* 지붕 경사 */ roofSlope: string | null + /* 주택 구조 */ houseStructure: string | null + /* 주택 구조 기타 */ houseStructureEtc: string | null + /* 서까래 재질*/ rafterMaterial: string | null + /* 서까래 재질 기타 */ rafterMaterialEtc: string | null + /* 서까래 크기 */ rafterSize: string | null + /* 서까래 크기 기타 */ rafterSizeEtc: string | null + /* 서까래 피치 */ rafterPitch: string | null + /* 서까래 피치 기타 */ rafterPitchEtc: string | null + /* 서까래 방향 */ rafterDirection: string | null + /* 노지판의 종류 */ openFieldPlateKind: string | null + /* 노지판의 종류 기타 */ openFieldPlateKindEtc: string | null + /* 노지판의 두께 */ openFieldPlateThickness: string | null + /* 누수 흔적 */ leakTrace: boolean | null + /* 방수재 종류*/ waterproofMaterial: string | null + /* 방수재 종류 기타 */ waterproofMaterialEtc: string | null + /* 단열재 유무 */ insulationPresence: string | null + /* 단열재 유무 기타 */ insulationPresenceEtc: string | null + /* 지붕 구조의 순서*/ structureOrder: string | null + /* 지붕 구조의 순서 기타 */ structureOrderEtc: string | null + /* 지붕 제품명 설치 가능 여부 확인*/ installationAvailability: string | null + /* 지붕 제품명 설치 가능 여부 확인 기타 */ installationAvailabilityEtc: string | null + /* 메모 */ memo: string | null + /* 등록 일시 */ regDt: Date + /* 수정 일시 */ uptDt: Date } +/** + * @description 조사 매물 생성 요청 파라미터 타입 + */ export type SurveyBasicRequest = { + /* 담당자명 */ representative: string + /* 담당자 ID */ representativeId: string | null + /* 판매점명 */ store: string | null + /* 판매점 ID */ storeId: string | null + /* 시공점명 */ constructionPoint: string | null + /* 시공점 ID */ constructionPointId: string | null + /* 조사 일자 */ investigationDate: string | null + /* 건물 이름 */ buildingName: string | null + /* 고객명 */ customerName: string | null + /* 우편번호 */ postCode: string | null + /* 주소 (도도부현) */ address: string | null + /* 상세 주소 */ addressDetail: string | null + /* 제출 상태 */ submissionStatus: boolean + /* 제출 일시 */ submissionDate: string | null + /* 제출 대상 판매점 ID */ submissionTargetId: string | null + /* 제출 대상 판매점명 */ submissionTargetNm: string | null - srlNo: string | null //판매점IDyyMMdd000 + /* 일련번호 */ + srlNo: string | null } +/** + * @description 조사 매물 상세 요청 파라미터 타입 + */ export type SurveyDetailRequest = { + /* 전기계약 용량 */ contractCapacity: string | null + /* 전기 소매 회사 */ retailCompany: string | null + /* 전기 부대 설비 */ supplementaryFacilities: string | null // number 배열 + /* 전기 부대 설비 기타 */ supplementaryFacilitiesEtc: string | null + /* 설치 희망 시스템 */ installationSystem: string | null + /* 설치 희망 시스템 기타 */ installationSystemEtc: string | null + /* 건축 년도 */ constructionYear: string | null + /* 건축 년도 기타 */ constructionYearEtc: string | null - roofMaterial: string | null // number 배열 + /* 지붕재 - 다중 선택 가능 [number]*/ + roofMaterial: string | null + /* 지붕재 기타 */ roofMaterialEtc: string | null + /* 지붕모양 */ roofShape: string | null + /* 지붕모양 기타 */ roofShapeEtc: string | null + /* 지붕 경사 */ roofSlope: string | null + /* 주택 구조 */ houseStructure: string | null + /* 주택 구조 기타 */ houseStructureEtc: string | null + /* 서까래 재질*/ rafterMaterial: string | null + /* 서까래 재질 기타 */ rafterMaterialEtc: string | null + /* 서까래 크기 */ rafterSize: string | null + /* 서까래 크기 기타 */ rafterSizeEtc: string | null + /* 서까래 피치 */ rafterPitch: string | null + /* 서까래 피치 기타 */ rafterPitchEtc: string | null + /* 서까래 방향 */ rafterDirection: string | null + /* 노지판의 종류 */ openFieldPlateKind: string | null + /* 노지판의 종류 기타 */ openFieldPlateKindEtc: string | null + /* 노지판의 두께 */ openFieldPlateThickness: string | null + /* 누수 흔적 */ leakTrace: boolean | null + /* 방수재 종류*/ waterproofMaterial: string | null + /* 방수재 종류 기타 */ waterproofMaterialEtc: string | null + /* 단열재 유무 */ insulationPresence: string | null + /* 단열재 유무 기타 */ insulationPresenceEtc: string | null + /* 지붕 구조의 순서*/ structureOrder: string | null + /* 지붕 구조의 순서 기타 */ structureOrderEtc: string | null + /* 지붕 제품명 설치 가능 여부 확인*/ installationAvailability: string | null + /* 지붕 제품명 설치 가능 여부 확인 기타 */ installationAvailabilityEtc: string | null + /* 메모 */ memo: string | null } -export type SurveyDetailCoverRequest = { - detailInfo: SurveyDetailRequest -} - +/** + * @description 조사 매물 등록 요청 파라미터 타입 + */ export type SurveyRegistRequest = { + /* 담당자명 */ representative: string + /* 담당자 ID */ representativeId: string | null + /* 판매점명 */ store: string | null + /* 판매점 ID */ storeId: string | null + /* 시공점명 */ constructionPoint: string | null + /* 조사 일자 */ investigationDate: string | null + /* 건물 이름 */ buildingName: string | null + /* 고객명 */ customerName: string | null + /* 우편번호 */ postCode: string | null + /* 주소 (도도부현) */ address: string | null + /* 상세 주소 */ addressDetail: string | null + /* 제출 상태 */ submissionStatus: boolean + /* 제출 일시 */ submissionDate: string | null + /* 조사 매물 상세 데이터 */ detailInfo: SurveyDetailRequest | null + /* 제출 대상 판매점 ID */ submissionTargetId: string | null - srlNo: string | null //판매점IDyyMMdd000 + /* 일련번호 */ + srlNo: string | null } -export type Mode = 'CREATE' | 'EDIT' | 'READ' | 'SUBMIT' // 등록 | 수정 | 상세 | 제출 +/** + * @description 조사 매물 페이지 모드 + */ +export type Mode = 'CREATE' | 'EDIT' | 'READ' | 'SUBMIT' export type SubmitTargetResponse = { + /* 제출 대상 판매점 ID */ targetStoreId: string + /* 제출 대상 판매점명 */ targetStoreNm: string + /* 담당자 ID */ repUserId: string + /* 담당자 이메일 */ repUserEmail: string + /* 권한 */ auth: string }