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/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/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/submission/admin-sub/route.ts b/src/app/api/submission/admin-sub/route.ts new file mode 100644 index 0000000..c8c30f3 --- /dev/null +++ b/src/app/api/submission/admin-sub/route.ts @@ -0,0 +1,42 @@ +import { prisma } from '@/libs/prisma' +import { NextRequest, NextResponse } from 'next/server' + +type AdminSubPerson = { + storeId: string + userId: string + eMail: string + authority: string +} +// 2차점이 자신에게 매핑 된 1차 판매점과 관리자 정보 조회 +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 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 = '${id}' 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..41c66ce --- /dev/null +++ b/src/app/api/submission/builder/route.ts @@ -0,0 +1,48 @@ +import { prisma } from '@/libs/prisma' +import { SubmitTargetResponse } from '@/types/Survey' +import { NextRequest, NextResponse } from 'next/server' + +type BuilderPerson = { + agencyStoreId: string + userId: string + eMail: string + userAuthCd: string +} + +// 2차점의 시공권한 user가 해당 판매점의 관리자 정보 조회 +// N == 일반유저, S == 수퍼유저, B == 시공권한유저 +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 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 = '${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) + + // 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/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 }) + } +} diff --git a/src/app/api/survey-sales/[id]/route.ts b/src/app/api/survey-sales/[id]/route.ts index d70471a..61a7b1c 100644 --- a/src/app/api/survey-sales/[id]/route.ts +++ b/src/app/api/survey-sales/[id]/route.ts @@ -1,100 +1,170 @@ 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 -} +/** + * @description 조사 매물 조회 에러 메시지 + */ +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 -} +/** + * @description T01 조회 권한 체크 + * @param {any} survey 조사 매물 데이터 + * @returns {boolean} 조사 매물 임시 저장 여부 + */ +const checkT01Role = (survey: any): boolean => survey.SRL_NO !== '一時保存' -const checkT01Role = (survey: Survey): boolean => survey.SRL_NO !== '一時保存' - -const checkAdminRole = (survey: Survey, storeId: string | null): boolean => { +/** + * @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 - - 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 => { +/** + * @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 - - 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 => { +/** + * @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 } -const checkRole = (survey: Survey, sessionParams: SessionParams): boolean => { - if (!survey || !sessionParams.role) return false +/** + * @description 권한 체크 + * @param {any} survey 조사 매물 데이터 + * @param {any} session 세션 데이터 + * @returns {boolean} 권한 존재 여부 + */ +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 } +/** + * @description 조사 매물 조회 + * @param {number} id 조사 매물 ID + * @returns {Promise} 조사 매물 데이터 + */ +const fetchSurvey = async (id: number) => { + // @ts-ignore + return await prisma.SD_SURVEY_SALES_BASIC_INFO.findFirst({ + where: { ID: id }, + include: { DETAIL_INFO: true }, + }) +} + +/** + * @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() + 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'), + 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 (checkRole(survey, sessionParams)) { + + /** pdf 데이터 요청 여부, 권한 여부 확인 */ + 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 }) } } +/** + * @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' : '' @@ -104,7 +174,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: { @@ -124,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 @@ -150,10 +253,27 @@ 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 }) } } - +/** + * @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 @@ -180,13 +300,48 @@ 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 }) } } +/** + * @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 @@ -205,6 +360,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..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,19 +124,77 @@ 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 }) + } + 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 핸들러 - 설문 목록 조회 + * @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'), @@ -154,25 +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, @@ -180,30 +239,52 @@ 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 } }) } catch (error) { console.error(error) - return NextResponse.json({ error: 'Fail Read Survey' }, { status: 500 }) + return NextResponse.json({ error: 'データ照会に失敗しました。' }, { status: 500 }) } } /** - * 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, @@ -218,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() @@ -231,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 + @@ -253,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 @@ -273,6 +390,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/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 71d2922..0030612 100644 --- a/src/components/pdf/SurveySaleDownloadPdf.tsx +++ b/src/components/pdf/SurveySaleDownloadPdf.tsx @@ -6,20 +6,28 @@ 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) + /** 페이지 랜더링 이후 PDF 생성 */ 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 +56,20 @@ export default function SurveySaleDownloadPdf() { }, } - generatePDF(targetRef, options).then(() => { - setIsShow(false) - router.replace(`/survey-sale/${id}`) - alert('PDFの生成が完了しました。 ポップアップウィンドウからダウンロードしてください。') - }) + /** 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 ( @@ -60,8 +77,8 @@ export default function SurveySaleDownloadPdf() {
({ 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 + ')', }) @@ -80,6 +94,7 @@ export default function SurveySaleSubmitPopup() { setSubmitData((prev) => ({ ...prev, [field]: value })) } + /** 필수값 검증 */ const validateData = (data: SubmitFormData): boolean => { const requiredFields = FORM_FIELDS.filter((field) => field.required) @@ -96,7 +111,7 @@ export default function SurveySaleSubmitPopup() { return true } - // TODO: Admin_Sub 계정 매핑된 submit target id 추가!!!! && 메일 테스트트 + /** 제출 처리 - 데이터 검증 이후 메일 전송 완료되면 데이터 저장 */ const handleSubmit = () => { if (validateData(submitData)) { window.neoConfirm('送信しますか? 送信後は変更・修正することはできません。', () => { @@ -109,7 +124,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) } }) @@ -118,7 +133,6 @@ export default function SurveySaleSubmitPopup() { alert('メール送信に失敗しました。 再度送信してください。') }) .finally(() => { - submitSurvey({ targetId: submitData.targetId, targetNm: submitData.targetNm }) setIsShow(false) popupController.setSurveySaleSubmitPopup(false) }) @@ -132,6 +146,7 @@ export default function SurveySaleSubmitPopup() { popupController.setSurveySaleSubmitPopup(false) } + /** 권한 별 폼 필드 렌더링 */ const renderFormField = (field: FormField) => { const isReadOnly = 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..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,25 +15,19 @@ 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(() => { - setBasicInfoSelected() - }, []) - - // 주소 데이터가 변경될 때만 업데이트 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 ( @@ -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 2088629..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' @@ -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() @@ -64,14 +62,16 @@ 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.userNm === basicData.representative + const isWriter = session.userId === basicData.representativeId const isReceiver = session?.storeId === basicData.submissionTargetId return { isSubmiter, isWriter, isReceiver } } + /** 제출 권한 체크 */ const calculateSubmitPermission = (session: any, basicData: SurveyBasicRequest): boolean => { switch (session?.role) { case 'T01': @@ -87,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() !== '' @@ -98,30 +99,34 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { } } + /** 임시 저장 로직 */ const tempSaveProcess = async () => { - if (idParam) { + /**route 에 id 가 있는 경우 업데이트, 없는 경우 생성 */ + 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('一時保存されました。') } + /** 입력 필드 포커스 처리 */ 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) @@ -130,28 +135,31 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { } } + /** 저장 성공 로직 */ const handleSuccessfulSave = async (isSubmitProcess?: boolean) => { - if (idParam) { - await updateSurvey({ survey: saveData, isTemporary: false, storeId: session.storeId ?? '' }) + /** route 에 id 가 있는 경우 업데이트, 없는 경우 생성 */ + 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}`) + /** 제출 로직인 경우 search param 추가 */ + 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) => { if (emptyField?.includes('Unit')) { alert('電気契約容量の単位を入力してください。') @@ -161,8 +169,9 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { focusInput(emptyField as keyof SurveyDetailInfo) } + /** 삭제 로직 */ const handleDelete = async () => { - if (routeId) { + if (!Number.isNaN(id)) { window.neoConfirm('削除しますか?', async () => { await deleteSurvey() if (!isDeletingSurvey) { @@ -173,13 +182,14 @@ 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) }) @@ -190,8 +200,10 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { } } + /** 로그인 여부 체크 */ if (!session?.isLoggedIn) return null + /** 읽기 모드, 제출 된 데이터, 제출 권한자는 리스트 버튼만 표시 */ if (mode === 'READ' && isSubmit && permissions.isSubmiter) { return (
@@ -204,19 +216,25 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) { return ( <> + {/* 읽기 모드 버튼 처리 */} + {/* 작성자 - 수정, 삭제, 제출(미제출인 매물) 버튼 표시 */} + {/* 제출권한자 - 수정, 제출(미제출인 매물) 버튼 표시 */} + {/* 제출수신자 - 수정, 삭제 버튼 표시 */} {mode === 'READ' && (
- {(permissions.isWriter || permissions.isSubmiter || (permissions.isReceiver && isSubmit)) && ( - - )} + {(permissions.isWriter || permissions.isSubmiter || (permissions.isReceiver && isSubmit)) && } {(permissions.isWriter || (permissions.isReceiver && isSubmit)) && } {!isSubmit && permissions.isSubmiter && }
)} + {/* 수정, 작성 모드 */} + {/* 작성자 - 임시저장, 저장, 제출(미제출인 매물) 버튼 표시 */} + {/* 제출권한자 - 임시저장, 저장, 제출(미제출인 매물) 버튼 표시 */} + {/* 제출수신자 - 임시저장, 저장 버튼 표시 */} {(mode === 'CREATE' || mode === 'EDIT') && (
@@ -243,14 +261,12 @@ const ListButton = () => { ) } -const EditButton = ({ setMode, id }: { setMode: (mode: Mode) => void; id: string }) => { - const router = useRouter() +const EditButton = ({ setMode }: { setMode: (mode: Mode) => void }) => { return (
@@ -95,7 +76,6 @@ export default function DataTable() {
- ) } diff --git a/src/components/survey-sale/detail/DetailForm.tsx b/src/components/survey-sale/detail/DetailForm.tsx index 8fd1ead..0f73c46 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, validateSurveyDetail } = useSurvey(Number(id)) + 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,7 +94,16 @@ export default function DetailForm() { })) const [roofInfoData, setRoofInfoData] = useState(roofInfoForm) - // 세션 데이터가 변경될 때 기본 정보 업데이트 + /** 제출 팝업 처리 - createSurvey 이후 popup 처리 시 노드 삽입 오류로 인해 별도 처리 */ + useEffect(() => { + const show = searchParams.get('show') + if (show === 'true') { + popupController.setSurveySaleSubmitPopup(true) + router.replace(pathname) + } + }, [searchParams, pathname]) + + /** 세션 데이터가 변경될 때 기본 정보 업데이트 */ useEffect(() => { if (!session?.isLoggedIn) return setBasicInfoData((prev) => ({ @@ -103,10 +117,9 @@ export default function DetailForm() { })) }, [session?.isLoggedIn]) - // 설문 데이터 로딩 및 업데이트 + /** 조사매물 상세 데이터 업데이트 */ useEffect(() => { - if (isLoadingSurveyDetail || !session?.isLoggedIn) return - if (surveyDetail && (mode === 'EDIT' || mode === 'READ')) { + if (!isLoadingSurveyDetail && surveyDetail && (mode === 'EDIT' || mode === 'READ')) { const { id, uptDt, regDt, detailInfo, ...rest } = surveyDetail setBasicInfoData((prev) => ({ ...prev, @@ -116,12 +129,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, @@ -132,6 +142,7 @@ export default function DetailForm() { return ( <> + {mode === 'READ' && 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/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/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 23b433e..cd32c87 100644 --- a/src/hooks/useInquiry.ts +++ b/src/hooks/useInquiry.ts @@ -4,7 +4,23 @@ 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' +/** + * @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, @@ -22,7 +38,46 @@ export function useInquiry( const { inquiryListRequest, offset } = useInquiryFilterStore() const { session } = useSessionStore() 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) + 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 + } + } + + /** + * @description 문의사항 목록 조회 + * + * @returns {Object} 문의사항 목록 데이터 + * @returns {InquiryList[]} data - 문의사항 목록 + * @returns {boolean} isLoading - 문의사항 목록 로딩 상태 + */ const { data: inquiryList, isLoading: isLoadingInquiryList } = useQuery({ queryKey: ['inquiryList', inquiryListRequest, offset], queryFn: async () => { @@ -32,13 +87,19 @@ export function useInquiry( }) return resp.data.data } catch (error: any) { - console.error(error.response.data) + errorRouter(error) return [] } }, enabled: !!inquiryListRequest, }) + /** + * @description 문의사항 목록 데이터 메모이제이션 + * + * @returns {Object} 메모이제이션된 문의사항 목록 데이터 + * @returns {InquiryList[]} inquiryList - 문의사항 목록 + */ const inquriyListData = useMemo(() => { if (isLoadingInquiryList) { return { inquiryList: [] } @@ -48,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 () => { @@ -57,13 +125,19 @@ export function useInquiry( }) return resp.data.data } catch (error: any) { - console.error(error.response) + errorRouter(error) return null } }, 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) @@ -72,8 +146,18 @@ export function useInquiry( onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['inquiryList'] }) }, + onError: (error: any) => { + errorRouter(error) + }, }) + /** + * @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}`) @@ -90,12 +174,18 @@ export function useInquiry( return blob } catch (error) { - console.error('File download error:', error) alert('ファイルのダウンロードに失敗しました') return null } } + /** + * @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 20f304c..5e50e30 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' @@ -54,8 +54,28 @@ type ZipCode = { kana2: string kana3: string } - -export function useSurvey(id?: number): { +/** + * @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, +): { surveyList: { data: SurveyBasicInfo[]; count: number } | {} surveyDetail: SurveyBasicInfo | null isLoadingSurveyList: boolean @@ -65,12 +85,14 @@ export function useSurvey(id?: number): { 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() const { keyword, searchOption, isMySurvey, sort, offset } = useSurveyFilterStore() @@ -78,33 +100,46 @@ export function useSurvey(id?: number): { 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 - } + /** + * @description 조사 매물 목록, 상세 데이터 조회 에러 처리 + * + * @param {any} error 에러 객체 + * @returns {void} 라우팅 처리 + */ + 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 } - alert('ログインしていません。') - return false } + /** + * @description 조사 매물 목록 조회 + * + * @returns {Object} 조사 매물 목록 데이터 + * @returns {SurveyBasicInfo[]} 조사 매물 목록 데이터 + * @returns {number} 조건에 맞는 조사 매물 총 개수 + * @returns {() => void} 조사 매물 목록 데이터 새로고침 함수 + * @returns {boolean} 조사 매물 목록 로딩 상태 + */ const { data: surveyListData, isLoading: isLoadingSurveyList, @@ -112,26 +147,34 @@ export function useSurvey(id?: number): { } = useQuery({ queryKey: ['survey', 'list', keyword, searchOption, isMySurvey, sort, offset, session?.storeNm, session?.builderId, session?.role], queryFn: async () => { - if (!checkSession()) { - router.replace('/') + 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 }, - enabled: session?.isLoggedIn, }) + + /** + * @description 조사 매물 목록 데이터 메모이제이션 + * + * @returns {Object} 메모이제이션된 조사 매물 목록 데이터 + * @returns {number} count - 조건에 맞는 조사 매물 총 개수 + * @returns {SurveyBasicInfo[]} data - 조사 매물 목록 데이터 + */ const surveyData = useMemo(() => { if (!surveyListData) return { count: 0, data: [] } return { @@ -139,38 +182,48 @@ export function useSurvey(id?: number): { } }, [surveyListData]) - const { data: surveyDetail, isLoading: isLoadingSurveyDetail } = useQuery({ + /** + * @description 조사 매물 상세 데이터 조회 + * + * @returns {Object} 조사 매물 상세 데이터 + * @returns {SurveyBasicInfo} surveyDetail - 조사 매물 상세 데이터 + * @returns {boolean} isLoadingSurveyDetail - 조사 매물 상세 데이터 로딩 상태 + * @returns {() => void} refetchSurveyDetail - 조사 매물 상세 데이터 새로고침 함수 + */ + const { + data: surveyDetail, + isLoading: isLoadingSurveyDetail, + refetch: refetchSurveyDetail, + } = useQuery({ queryKey: ['survey', id], queryFn: async () => { - if (!checkSession()) { - router.replace('/survey-sale') - 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 && session?.isLoggedIn, + 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('/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 @@ -181,8 +234,18 @@ export function useSurvey(id?: number): { }, }) + /** + * @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 }) => { + 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, @@ -198,6 +261,16 @@ export function useSurvey(id?: number): { }, }) + /** + * @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') @@ -206,10 +279,21 @@ export function useSurvey(id?: number): { }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['survey', 'list'] }) - queryClient.invalidateQueries({ queryKey: ['survey', id] }) + }, + onError: (error: any) => { + alert(error.response?.data.error) }, }) + /** + * @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') @@ -223,15 +307,22 @@ export function useSurvey(id?: number): { queryClient.invalidateQueries({ queryKey: ['survey', 'list'] }) queryClient.invalidateQueries({ queryKey: ['survey', id] }) }, + onError: (error: any) => { + alert(error.response?.data.error) + }, }) + /** + * @description 조사 매물 상세 데이터 유효성 검사 + * + * @param {SurveyDetailRequest} surveyDetail 검사할 조사 매물 상세 데이터 + * @returns {string} 빈 필드 이름 또는 빈 문자열 + */ 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() === '' } @@ -266,6 +357,13 @@ export function useSurvey(id?: number): { return '' } + /** + * @description 우편번호 검색 + * + * @param {string} zipCode 검색할 우편번호 + * @returns {Promise} 우편번호 검색 결과 + * @throws {Error} 우편번호 검색 실패 시 에러 발생 + */ const getZipCode = async (zipCode: string): Promise => { try { const { data } = await axiosInstance(null).get( @@ -278,6 +376,40 @@ export function useSurvey(id?: number): { } } + /** + * @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) { + 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) { + alert(error.response?.data.error) + return null + } + } + return { surveyList: surveyData.data, surveyDetail: surveyDetail as SurveyBasicInfo | null, @@ -293,6 +425,8 @@ export function useSurvey(id?: number): { submitSurvey, validateSurveyDetail, getZipCode, + getSubmitTarget, refetchSurveyList, + refetchSurveyDetail, } } 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 2db1034..d46dae7 100644 --- a/src/types/Survey.ts +++ b/src/types/Survey.ts @@ -1,148 +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' | 'TEMP' // 등록 | 수정 | 상세 | 임시저장 +/** + * @description 조사 매물 페이지 모드 + */ +export type Mode = 'CREATE' | 'EDIT' | 'READ' | 'SUBMIT' + +export type SubmitTargetResponse = { + /* 제출 대상 판매점 ID */ + targetStoreId: string + /* 제출 대상 판매점명 */ + targetStoreNm: string + /* 담당자 ID */ + repUserId: string + /* 담당자 이메일 */ + repUserEmail: string + /* 권한 */ + auth: string +}