Compare commits

..

No commits in common. "2e0ff4ae6ff089da81805113b4de46150c9f9756" and "4a7051fdd16f91452b5fb6fc05e909ebf8642929" have entirely different histories.

47 changed files with 453 additions and 1007 deletions

View File

@ -1,4 +1,3 @@
NEXT_PUBLIC_RUN_MODE=development
# 모바일 디바이스로 로컬 서버 확인하려면 자신 IP 주소로 변경 # 모바일 디바이스로 로컬 서버 확인하려면 자신 IP 주소로 변경
# 다시 로컬에서 개발할때는 localhost로 변경 # 다시 로컬에서 개발할때는 localhost로 변경
#route handler #route handler

View File

@ -1,18 +0,0 @@
NEXT_PUBLIC_RUN_MODE=local
# 모바일 디바이스로 로컬 서버 확인하려면 자신 IP 주소로 변경
# 다시 로컬에서 개발할때는 localhost로 변경
#route handler
NEXT_PUBLIC_API_URL=http://localhost:3000
#qsp 로그인 api
NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120
#1:1문의 api
NEXT_PUBLIC_INQUIRY_API_URL=http://1.248.227.176:38080
#QPARTNER 로그인 api
DB_HOST=202.218.61.226
DB_USER=readonly
DB_PASSWORD=aAjmFW12iHKW84l1
DB_DATABASE=qpartners
DB_PORT=3306

View File

@ -1,6 +1,5 @@
NEXT_PUBLIC_RUN_MODE=production
#route handler #route handler
NEXT_PUBLIC_API_URL=http://1.248.227.176:3000 NEXT_PUBLIC_API_URL=http://172.30.1.35:3000
#qsp 로그인 api #qsp 로그인 api
NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120

View File

@ -3,15 +3,9 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "env-cmd -f .env.localhost next dev --turbopack", "dev": "next dev --turbopack",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"build:local": "env-cmd -f .env.localhost next build",
"build:dev": "env-cmd -f .env.development next build",
"build:prod": "env-cmd -f .env.production next build",
"start:local": "env-cmd -f .env.localhost next start",
"start:dev": "env-cmd -f .env.development next start",
"start:prod": "env-cmd -f .env.production next start",
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
@ -19,7 +13,6 @@
"@tanstack/react-query": "^5.71.0", "@tanstack/react-query": "^5.71.0",
"@tanstack/react-query-devtools": "^5.71.0", "@tanstack/react-query-devtools": "^5.71.0",
"axios": "^1.8.4", "axios": "^1.8.4",
"env-cmd": "^10.1.0",
"iron-session": "^8.0.4", "iron-session": "^8.0.4",
"lucide": "^0.503.0", "lucide": "^0.503.0",
"mssql": "^11.0.1", "mssql": "^11.0.1",

63
pnpm-lock.yaml generated
View File

@ -20,9 +20,6 @@ importers:
axios: axios:
specifier: ^1.8.4 specifier: ^1.8.4
version: 1.8.4 version: 1.8.4
env-cmd:
specifier: ^10.1.0
version: 10.1.0
iron-session: iron-session:
specifier: ^8.0.4 specifier: ^8.0.4
version: 8.0.4 version: 8.0.4
@ -804,10 +801,6 @@ packages:
resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
engines: {node: '>=16'} engines: {node: '>=16'}
commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
cookie@0.7.2: cookie@0.7.2:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@ -815,10 +808,6 @@ packages:
core-js@3.41.0: core-js@3.41.0:
resolution: {integrity: sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==} resolution: {integrity: sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
css-line-break@2.1.0: css-line-break@2.1.0:
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
@ -877,11 +866,6 @@ packages:
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
env-cmd@10.1.0:
resolution: {integrity: sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA==}
engines: {node: '>=8.0.0'}
hasBin: true
es-define-property@1.0.1: es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -1037,9 +1021,6 @@ packages:
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
engines: {node: '>=16'} engines: {node: '>=16'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
jiti@2.4.2: jiti@2.4.2:
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
hasBin: true hasBin: true
@ -1236,10 +1217,6 @@ packages:
resolution: {integrity: sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==} resolution: {integrity: sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==}
engines: {node: '>=18'} engines: {node: '>=18'}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
performance-now@2.1.0: performance-now@2.1.0:
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
@ -1347,14 +1324,6 @@ packages:
resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
simple-swizzle@0.2.2: simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
@ -1454,11 +1423,6 @@ packages:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true hasBin: true
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
zustand@5.0.3: zustand@5.0.3:
resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==} resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==}
engines: {node: '>=12.20.0'} engines: {node: '>=12.20.0'}
@ -2106,19 +2070,11 @@ snapshots:
commander@11.1.0: {} commander@11.1.0: {}
commander@4.1.1: {}
cookie@0.7.2: {} cookie@0.7.2: {}
core-js@3.41.0: core-js@3.41.0:
optional: true optional: true
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
css-line-break@2.1.0: css-line-break@2.1.0:
dependencies: dependencies:
utrie: 1.0.2 utrie: 1.0.2
@ -2167,11 +2123,6 @@ snapshots:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
tapable: 2.2.1 tapable: 2.2.1
env-cmd@10.1.0:
dependencies:
commander: 4.1.1
cross-spawn: 7.0.6
es-define-property@1.0.1: {} es-define-property@1.0.1: {}
es-errors@1.3.0: {} es-errors@1.3.0: {}
@ -2346,8 +2297,6 @@ snapshots:
dependencies: dependencies:
is-inside-container: 1.0.0 is-inside-container: 1.0.0
isexe@2.0.0: {}
jiti@2.4.2: {} jiti@2.4.2: {}
js-md4@0.3.2: {} js-md4@0.3.2: {}
@ -2551,8 +2500,6 @@ snapshots:
is-inside-container: 1.0.0 is-inside-container: 1.0.0
is-wsl: 3.1.0 is-wsl: 3.1.0
path-key@3.1.1: {}
performance-now@2.1.0: performance-now@2.1.0:
optional: true optional: true
@ -2675,12 +2622,6 @@ snapshots:
'@img/sharp-win32-x64': 0.33.5 '@img/sharp-win32-x64': 0.33.5
optional: true optional: true
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
simple-swizzle@0.2.2: simple-swizzle@0.2.2:
dependencies: dependencies:
is-arrayish: 0.3.2 is-arrayish: 0.3.2
@ -2762,10 +2703,6 @@ snapshots:
uuid@8.3.2: {} uuid@8.3.2: {}
which@2.0.2:
dependencies:
isexe: 2.0.0
zustand@5.0.3(@types/react@19.0.12)(react@19.1.0): zustand@5.0.3(@types/react@19.0.12)(react@19.1.0):
optionalDependencies: optionalDependencies:
'@types/react': 19.0.12 '@types/react': 19.0.12

View File

@ -0,0 +1,3 @@
<svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 5L5 1L9 5" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 207 B

View File

@ -12,6 +12,7 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: '페이지 번호와 페이지당 아이템 수가 필요합니다' }, { status: 400 }) return NextResponse.json({ error: '페이지 번호와 페이지당 아이템 수가 필요합니다' }, { status: 400 })
} }
const ids = searchParams.get('ids')
const category = searchParams.get('category') const category = searchParams.get('category')
const keyword = searchParams.get('keyword') const keyword = searchParams.get('keyword')
@ -19,13 +20,14 @@ export async function GET(request: NextRequest) {
SELECT SELECT
msm.id msm.id
, msm.product_name , msm.product_name
, details.detail_cnt , msm.manu_ft_cd
, msm.roof_mt_cd
, msm.roof_sh_cd
, details.detail , details.detail
FROM ms_suitable_main msm FROM ms_suitable_main msm
LEFT JOIN ( LEFT JOIN (
SELECT SELECT
msd.main_id msd.main_id
, COUNT(msd.id) AS detail_cnt
, ( , (
SELECT SELECT
msd_json.id msd_json.id
@ -40,7 +42,9 @@ export async function GET(request: NextRequest) {
GROUP BY msd.main_id GROUP BY msd.main_id
) AS details ) AS details
ON msm.id = details.main_id ON msm.id = details.main_id
--mainIds AND details.main_id IN (:mainIds)
WHERE 1=1 WHERE 1=1
--mainIds AND msm.id IN (:mainIds)
--roofMtCd AND msm.roof_mt_cd = ':roofMtCd' --roofMtCd AND msm.roof_mt_cd = ':roofMtCd'
--productName AND msm.product_name LIKE '%:productName%' --productName AND msm.product_name LIKE '%:productName%'
ORDER BY msm.product_name ORDER BY msm.product_name
@ -49,6 +53,10 @@ export async function GET(request: NextRequest) {
` `
// 검색 조건 설정 // 검색 조건 설정
if (ids) {
query = query.replaceAll('--mainIds ', '')
query = query.replaceAll(':mainIds', ids)
}
if (category) { if (category) {
query = query.replace('--roofMtCd ', '') query = query.replace('--roofMtCd ', '')
query = query.replace(':roofMtCd', category) query = query.replace(':roofMtCd', category)
@ -58,6 +66,7 @@ export async function GET(request: NextRequest) {
query = query.replace(':productName', keyword) query = query.replace(':productName', keyword)
} }
// @ts-ignore
const suitable: Suitable[] = await prisma.$queryRawUnsafe(query, pageNumber, itemPerPage) const suitable: Suitable[] = await prisma.$queryRawUnsafe(query, pageNumber, itemPerPage)
return NextResponse.json(suitable) return NextResponse.json(suitable)

View File

@ -19,55 +19,34 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
} }
} }
const getNewSrlNo = async (srlNo: string, storeId: string) => {
let newSrlNo = srlNo
console.log('srlNo:: ', srlNo)
if (srlNo.startsWith('一時保存')) {
//@ts-ignore
const lastSurvey = await prisma.SD_SURVEY_SALES_BASIC_INFO.findFirst({
where: {
SRL_NO: {
startsWith: storeId,
},
},
orderBy: {
ID: 'desc',
},
})
const lastNo = lastSurvey ? parseInt(lastSurvey.SRL_NO.slice(-3)) : 0
newSrlNo =
storeId +
new Date().getFullYear().toString().slice(-2) +
(new Date().getMonth() + 1).toString().padStart(2, '0') +
new Date().getDate().toString().padStart(2, '0') +
(lastNo + 1).toString().padStart(3, '0')
}
return newSrlNo
}
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try { try {
const { id } = await params const { id } = await params
const body = await request.json() const body = await request.json()
const { detailInfo, ...basicInfo } = body.survey const { DETAIL_INFO, ...basicInfo } = body
// PUT 요청 시 임시저장 여부 확인 후 임시저장 시 기존 SRL_NO 사용, 기본 저장 시 새로운 SRL_NO 생성 console.log('body:: ', body)
const newSrlNo = body.isTemporary ? body.survey.srlNo : await getNewSrlNo(body.survey.srlNo, body.storeId)
// @ts-ignore // @ts-ignore
const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({
where: { ID: Number(id) }, where: { ID: Number(id) },
data: { data: {
...convertToSnakeCase(basicInfo), ...convertToSnakeCase(basicInfo),
SRL_NO: newSrlNo,
UPT_DT: new Date(), UPT_DT: new Date(),
DETAIL_INFO: { DETAIL_INFO: DETAIL_INFO ? {
update: convertToSnakeCase(detailInfo), upsert: {
}, create: convertToSnakeCase(DETAIL_INFO),
update: convertToSnakeCase(DETAIL_INFO),
where: {
BASIC_INFO_ID: Number(id)
}
}
} : undefined
}, },
include: { include: {
DETAIL_INFO: true, DETAIL_INFO: true
}, }
}) })
console.log('survey:: ', survey)
return NextResponse.json(survey) return NextResponse.json(survey)
} catch (error) { } catch (error) {
console.error('Error updating survey:', error) console.error('Error updating survey:', error)
@ -113,24 +92,49 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
const { id } = await params const { id } = await params
const body = await request.json() const body = await request.json()
// 제출 시 기존 SRL_NO 확인 후 '임시저장'으로 시작하면 새로운 SRL_NO 생성 if (body.submit) {
const newSrlNo = await getNewSrlNo(body.srlNo, body.storeId)
if (body.targetId) {
// @ts-ignore // @ts-ignore
const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({
where: { ID: Number(id) }, where: { ID: Number(id) },
data: { data: {
SUBMISSION_STATUS: true, SUBMISSION_STATUS: true,
SUBMISSION_DATE: new Date(), SUBMISSION_DATE: new Date(),
SUBMISSION_TARGET_ID: body.targetId,
UPT_DT: new Date(), UPT_DT: new Date(),
SRL_NO: newSrlNo,
}, },
}) })
console.log(survey)
return NextResponse.json({ message: 'Survey confirmed successfully' }) return NextResponse.json({ message: 'Survey confirmed successfully' })
} }
// } else {
// // @ts-ignore
// const hasDetails = await prisma.SD_SURVEY_SALES_DETAIL_INFO.findUnique({
// where: { BASIC_INFO_ID: Number(id) },
// })
// if (hasDetails) {
// //@ts-ignore
// const result = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({
// where: { ID: Number(id) },
// data: {
// UPT_DT: new Date(),
// DETAIL_INFO: {
// update: convertToSnakeCase(body.DETAIL_INFO),
// },
// },
// })
// return NextResponse.json(result)
// } else {
// // @ts-ignore
// const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({
// where: { ID: Number(id) },
// data: {
// DETAIL_INFO: {
// create: convertToSnakeCase(body.DETAIL_INFO),
// },
// },
// })
// return NextResponse.json({ message: 'Survey detail created successfully' })
// }
// }
} catch (error) { } catch (error) {
console.error('Error updating survey:', error) console.error('Error updating survey:', error)
return NextResponse.json({ error: 'Failed to update survey' }, { status: 500 }) return NextResponse.json({ error: 'Failed to update survey' }, { status: 500 })

View File

@ -1,7 +1,6 @@
import { NextResponse } from 'next/server' import { NextResponse } from 'next/server'
import { prisma } from '@/libs/prisma' import { prisma } from '@/libs/prisma'
import { convertToSnakeCase } from '@/utils/common-utils' import { convertToSnakeCase } from '@/utils/common-utils'
import { equal } from 'assert'
/** /**
* *
*/ */
@ -88,14 +87,13 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => {
switch (params.role) { switch (params.role) {
case 'Admin': // 1차점 case 'Admin': // 1차점
// 같은 판매점에서 작성된 매물 + 2차점에서 제출받은 매물
where.OR = [ where.OR = [
{ {
// 같은 판매점에서 작성한 제출/제출되지 않은 매물
AND: [{ STORE: { equals: params.store } }], AND: [{ STORE: { equals: params.store } }],
}, },
{ {
// MUSUBI (시공권한 X) 가 ORDER 에 제출한 매물 AND: [{ STORE: { startsWith: params.store } }, { SUBMISSION_STATUS: { equals: true } }],
AND: [{ SUBMISSION_TARGET_ID: { equals: params.store } }, { SUBMISSION_STATUS: { equals: true } }],
}, },
] ]
break break
@ -103,7 +101,6 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => {
case 'Admin_Sub': // 2차점 case 'Admin_Sub': // 2차점
where.OR = [ where.OR = [
{ {
// MUSUBI (시공권한 X) 같은 판매점에서 작성한 제출/제출되지 않은 매물
AND: [ AND: [
{ STORE: { equals: params.store } }, { STORE: { equals: params.store } },
{ {
@ -112,9 +109,8 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => {
], ],
}, },
{ {
// MUSUBI (시공권한 O) 가 MUSUBI 에 제출한 매물 + PARTNER 가 제출한 매물
AND: [ AND: [
{ SUBMISSION_TARGET_ID: { equals: params.store } }, { STORE: { equals: params.store } },
{ CONSTRUCTION_POINT: { not: null } }, { CONSTRUCTION_POINT: { not: null } },
{ CONSTRUCTION_POINT: { not: '' } }, { CONSTRUCTION_POINT: { not: '' } },
{ SUBMISSION_STATUS: { equals: true } }, { SUBMISSION_STATUS: { equals: true } },
@ -123,8 +119,8 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => {
] ]
break break
case 'Builder': // MUSUBI (시공권한 O) case 'Builder': // 2차점 시공권한
case 'Partner': // PARTNER case 'Partner': // Partner
// 같은 시공ID에서 작성된 매물 // 같은 시공ID에서 작성된 매물
where.AND?.push({ where.AND?.push({
CONSTRUCTION_POINT: { equals: params.builderNo }, CONSTRUCTION_POINT: { equals: params.builderNo },
@ -132,21 +128,6 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => {
break break
case 'T01': case 'T01':
where.OR = [
{
NOT: {
SRL_NO: {
startsWith: '一時保存',
},
},
},
{
STORE: {
equals: params.store,
},
},
]
break
case 'User': case 'User':
// 모든 매물 조회 가능 (추가 조건 없음) // 모든 매물 조회 가능 (추가 조건 없음)
break break
@ -241,44 +222,19 @@ export async function PUT(request: Request) {
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
const body = await request.json() const body = await request.json()
console.log('body:: ', body)
// 임시 저장 시 임시저장 + 000 으로 저장 const { detailInfo, ...basicInfo } = body
// 기본 저장 시 판매점ID + yyMMdd + 000 으로 저장
const baseSrlNo =
body.survey.srlNo ??
body.storeId +
new Date().getFullYear().toString().slice(-2) +
(new Date().getMonth() + 1).toString().padStart(2, '0') +
new Date().getDate().toString().padStart(2, '0')
// @ts-ignore // 기본 정보 생성
const lastSurvey = await prisma.SD_SURVEY_SALES_BASIC_INFO.findFirst({
where: {
SRL_NO: {
startsWith: body.storeId,
},
},
orderBy: {
SRL_NO: 'desc',
},
})
// 마지막 번호 추출
const lastNumber = lastSurvey ? parseInt(lastSurvey.SRL_NO.slice(-3)) : 0
// 새로운 srlNo 생성 - 임시저장일 경우 '임시저장' 으로 저장
const newSrlNo = baseSrlNo.startsWith('一時保存') ? baseSrlNo : baseSrlNo + (lastNumber + 1).toString().padStart(3, '0')
const { detailInfo, ...basicInfo } = body.survey
//@ts-ignore //@ts-ignore
const result = await prisma.SD_SURVEY_SALES_BASIC_INFO.create({ const result = await prisma.SD_SURVEY_SALES_BASIC_INFO.create({
data: { data: {
...convertToSnakeCase(basicInfo), ...convertToSnakeCase(basicInfo),
SRL_NO: newSrlNo,
DETAIL_INFO: { DETAIL_INFO: {
create: convertToSnakeCase(detailInfo), create: convertToSnakeCase(detailInfo)
}, }
}, }
}) })
return NextResponse.json(result) return NextResponse.json(result)
} catch (error) { } catch (error) {

View File

@ -113,10 +113,6 @@ export default function page() {
<input type="checkbox" id="ch06" disabled /> <input type="checkbox" id="ch06" disabled />
<label htmlFor="ch06">Check Box</label> <label htmlFor="ch06">Check Box</label>
</div> </div>
<div className="check-form-box space">
<input type="checkbox" id="ch07" defaultChecked />
<label htmlFor="ch07">Check Box</label>
</div>
</div> </div>
</div> </div>
<div className="design-box"> <div className="design-box">

View File

@ -5,8 +5,9 @@ import { useEffect, useReducer, useState } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useLocalStorage } from 'usehooks-ts' import { useLocalStorage } from 'usehooks-ts'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { axiosInstance } from '@/libs/axios'
import { useSessionStore } from '@/store/session' import { useSessionStore } from '@/store/session'
import { useAxios } from '@/hooks/useAxios'
interface AccountState { interface AccountState {
loginId: string loginId: string
pwd: string pwd: string
@ -23,8 +24,6 @@ export default function Login() {
//로그인 상태 //로그인 상태
const [isLogin, setIsLogin] = useState(false) const [isLogin, setIsLogin] = useState(false)
const { axiosInstance } = useAxios()
const { session, setSession } = useSessionStore() const { session, setSession } = useSessionStore()
const [value, setValue, removeValue] = useLocalStorage<{ indivisualData: string }>('hanasysIndivisualState', { indivisualData: '' }) const [value, setValue, removeValue] = useLocalStorage<{ indivisualData: string }>('hanasysIndivisualState', { indivisualData: '' })

View File

@ -2,16 +2,16 @@
import Image from 'next/image' import Image from 'next/image'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import SuitableList from './SuitableList' import SuitableListRaw from './SuitableList'
import { useSuitable } from '@/hooks/useSuitable' import { useSuitable } from '@/hooks/useSuitable'
import { useSuitableStore } from '@/store/useSuitableStore' import { useSuitableStore } from '@/store/useSuitableStore'
import type { CommCode } from '@/types/CommCode' import type { CommCode } from '@/types/CommCode'
import { SUITABLE_HEAD_CODE } from '@/types/Suitable' import { SUITABLE_HEAD_CODE } from '@/types/Suitable'
export default function Suitable() { export default function SuitableRaw() {
const [reference, setReference] = useState(true) const [reference, setReference] = useState(true)
const { getSuitableCommCode } = useSuitable() const { getSuitableCommCode, refetchBySearch } = useSuitable()
const { suitableCommCode, selectedCategory, setSelectedCategory, searchValue, setSearchValue, setIsSearch, clearSelectedItems } = useSuitableStore() const { suitableCommCode, selectedCategory, setSelectedCategory, searchValue, setSearchValue, setIsSearch, clearSelectedItems } = useSuitableStore()
const handleInputSearch = async () => { const handleInputSearch = async () => {
@ -20,13 +20,19 @@ export default function Suitable() {
return return
} }
setIsSearch(true) setIsSearch(true)
refetchBySearch()
} }
const handleInputClear = () => { const handleInputClear = () => {
setSearchValue('') setSearchValue('')
setIsSearch(false) setIsSearch(false)
refetchBySearch()
} }
useEffect(() => {
refetchBySearch()
}, [selectedCategory])
useEffect(() => { useEffect(() => {
getSuitableCommCode() getSuitableCommCode()
return () => { return () => {
@ -56,11 +62,6 @@ export default function Suitable() {
placeholder="屋根材 製品名を入力してください." placeholder="屋根材 製品名を入力してください."
value={searchValue} value={searchValue}
onChange={(e) => setSearchValue(e.target.value)} onChange={(e) => setSearchValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleInputSearch()
}
}}
/> />
{searchValue && <button className="del-icon" onClick={handleInputClear} />} {searchValue && <button className="del-icon" onClick={handleInputClear} />}
<button className="search-icon" onClick={handleInputSearch} /> <button className="search-icon" onClick={handleInputSearch} />
@ -109,7 +110,7 @@ export default function Suitable() {
</li> </li>
</ul> </ul>
</div> </div>
<SuitableList /> <SuitableListRaw />
</div> </div>
</div> </div>
) )

View File

@ -1,49 +1,79 @@
'use client' 'use client'
import Image from 'next/image' import Image from 'next/image'
import { useState, useEffect, useCallback, useRef, useMemo } from 'react' import { useState, useEffect, useRef, useCallback, useMemo } from 'react'
import SuitableButton from './SuitableButton' import SuitableButton from './SuitableButton'
import SuitableNoData from './SuitableNoData' import SuitableNoData from './SuitableNoData'
import { useSuitable } from '@/hooks/useSuitable' import { useSuitable } from '@/hooks/useSuitable'
import { useSuitableStore } from '@/store/useSuitableStore' import { useSuitableStore } from '@/store/useSuitableStore'
import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/Suitable' import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/Suitable'
// 한 번에 로드할 아이템 수
const ITEMS_PER_PAGE = 100
export default function SuitableList() { export default function SuitableList() {
const { toCodeName, toSuitableDetail, suitables, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useSuitable() const { toCodeName, suitableSearchResults, isSearchLoading, toSuitableDetail } = useSuitable()
const { selectedItems, addSelectedItem, removeSelectedItem } = useSuitableStore() const { selectedItems, addSelectedItem, removeSelectedItem } = useSuitableStore()
const [openItems, setOpenItems] = useState<Set<number>>(new Set()) const [openItems, setOpenItems] = useState<Set<number>>(new Set())
const [visibleItems, setVisibleItems] = useState<Suitable[]>([])
const [page, setPage] = useState(1)
const [isLoadingMore, setIsLoadingMore] = useState(false)
const observerTarget = useRef<HTMLDivElement>(null) const observerTarget = useRef<HTMLDivElement>(null)
// 선택된 아이템 확인 - 메인 하위 아이템 indeterminate 확인 // 선택된 아이템 확인 함수 메모이제이션
const isMainIndeterminate = useMemo(
() => (mainId: number, detailCnt: number) => {
const mainItem = selectedItems.get(mainId)
if (!mainItem) return false
return mainItem.size > 0 && mainItem.size < detailCnt
},
[selectedItems],
)
// 선택된 아이템 확인
const isItemSelected = useCallback( const isItemSelected = useCallback(
(mainId: number, detailId?: number): boolean => { (itemId: number) => {
const mainItem = selectedItems.get(mainId) return selectedItems.some((selected) => selected === itemId)
if (!mainItem) return false
if (!detailId) return true
return mainItem.has(detailId)
}, },
[selectedItems], [selectedItems],
) )
// 아이템 클릭 // 초기 데이터 로드
useEffect(() => {
if (suitableSearchResults) {
const initialItems = suitableSearchResults.slice(0, ITEMS_PER_PAGE)
setVisibleItems(initialItems)
setPage(1)
}
}, [suitableSearchResults])
// Intersection Observer 설정
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && suitableSearchResults && !isLoadingMore) {
const nextPage = page + 1
const startIndex = (nextPage - 1) * ITEMS_PER_PAGE
const endIndex = startIndex + ITEMS_PER_PAGE
const nextItems = suitableSearchResults.slice(startIndex, endIndex)
if (nextItems.length > 0) {
setIsLoadingMore(true)
setVisibleItems((prev) => [...prev, ...nextItems])
setPage(nextPage)
setIsLoadingMore(false)
}
}
},
{
threshold: 0.2,
},
)
if (observerTarget.current) {
observer.observe(observerTarget.current)
}
return () => observer.disconnect()
}, [page, suitableSearchResults, isLoadingMore])
const handleItemClick = useCallback( const handleItemClick = useCallback(
(mainId: number, detailId?: number): void => { (itemId: number) => {
isItemSelected(mainId, detailId) ? removeSelectedItem(mainId, detailId) : addSelectedItem(mainId, detailId) isItemSelected(itemId) ? removeSelectedItem(itemId) : addSelectedItem(itemId)
}, },
[isItemSelected, addSelectedItem, removeSelectedItem], [isItemSelected, addSelectedItem, removeSelectedItem],
) )
// 아이템 열기/닫기
const toggleItemOpen = useCallback((itemId: number) => { const toggleItemOpen = useCallback((itemId: number) => {
setOpenItems((prev) => { setOpenItems((prev) => {
const newOpenItems = new Set(prev) const newOpenItems = new Set(prev)
@ -54,26 +84,38 @@ export default function SuitableList() {
// TODO: 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요 // TODO: 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요
const suitableCheck = useCallback((value: string) => { const suitableCheck = useCallback((value: string) => {
const iconMap: Record<string, string> = { if (value === '×') {
'×': '/assets/images/sub/compliance_x_icon.svg',
: '/assets/images/sub/compliance_quest_icon.svg',
default: '/assets/images/sub/compliance_check_icon.svg',
}
return ( return (
<div className="compliance-icon"> <div className="compliance-icon">
<Image src={iconMap[value] || iconMap.default} width={22} height={22} alt="" /> <Image src={'/assets/images/sub/compliance_x_icon.svg'} width={22} height={22} alt=""></Image>
</div> </div>
) )
} else if (value === 'ー') {
return (
<div className="compliance-icon">
<Image src={'/assets/images/sub/compliance_quest_icon.svg'} width={22} height={22} alt=""></Image>
</div>
)
} else {
return (
<div className="compliance-icon">
<Image src={'/assets/images/sub/compliance_check_icon.svg'} width={22} height={22} alt=""></Image>
</div>
)
}
}, []) }, [])
// 아이템 렌더링 // 메모이제이션된 아이템 렌더링
const renderItem = useCallback( const renderItem = useCallback(
(item: Suitable) => { (item: Suitable) => {
const isSelected = isItemSelected(item.id)
const isOpen = openItems.has(item.id)
return ( return (
<div className={`compliance-check-bx ${openItems.has(item.id) ? 'act' : ''}`} key={item.id}> <div className={`compliance-check-bx ${isOpen ? 'act' : ''}`} key={item.id}>
<div className="check-name-wrap"> <div className="check-name-wrap">
<div className={`check-form-box ${isMainIndeterminate(item.id, item.detailCnt) ? 'space' : ''}`}> <div className="check-form-box ">
<input type="checkbox" id={`ch${item.id}`} checked={isItemSelected(item.id)} onChange={() => handleItemClick(item.id)} /> <input type="checkbox" id={`ch${item.id}`} checked={isSelected} onChange={() => handleItemClick(item.id)} />
<label htmlFor={`ch${item.id}`}>{item.productName}</label> <label htmlFor={`ch${item.id}`}>{item.productName}</label>
</div> </div>
<div className="check-name-btn"> <div className="check-name-btn">
@ -85,12 +127,7 @@ export default function SuitableList() {
<li className="reference-item" key={subItem.id}> <li className="reference-item" key={subItem.id}>
<div className="check-item-wrap"> <div className="check-item-wrap">
<div className="check-form-box light"> <div className="check-form-box light">
<input <input type="checkbox" id={`ch${subItem.id}`} />
type="checkbox"
id={`ch${subItem.id}`}
checked={isItemSelected(item.id, subItem.id)}
onChange={() => handleItemClick(item.id, subItem.id)}
/>
<label htmlFor={`ch${subItem.id}`}>{toCodeName(SUITABLE_HEAD_CODE.TRESTLE_MFPC_CD, subItem.trestleMfpcCd)}</label> <label htmlFor={`ch${subItem.id}`}>{toCodeName(SUITABLE_HEAD_CODE.TRESTLE_MFPC_CD, subItem.trestleMfpcCd)}</label>
</div> </div>
<div className="compliance-icon-wrap"> <div className="compliance-icon-wrap">
@ -111,38 +148,24 @@ export default function SuitableList() {
[isItemSelected, openItems, handleItemClick, toggleItemOpen, suitableCheck, toCodeName, toSuitableDetail], [isItemSelected, openItems, handleItemClick, toggleItemOpen, suitableCheck, toCodeName, toSuitableDetail],
) )
// 아이템 리스트 // 메모이제이션된 아이템 리스트
const suitableList = suitables?.pages.flat() ?? [] const renderedItems = useMemo(() => {
return visibleItems.map(renderItem)
}, [visibleItems, renderItem])
// Intersection Observer 설정 if (isSearchLoading) {
useEffect(() => { return <div>Loading...</div>
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage()
}
},
{
threshold: 0,
rootMargin: '100px',
},
)
if (observerTarget.current) {
observer.observe(observerTarget.current)
} }
return () => observer.disconnect() if (!suitableSearchResults?.length) {
}, [hasNextPage, isFetchingNextPage, fetchNextPage]) return <SuitableNoData />
}
if (isLoading) return <div>Loading...</div>
if (!suitableList.length) return <SuitableNoData />
return ( return (
<> <>
{suitableList.map(renderItem)} {renderedItems}
<div ref={observerTarget} className="loading-indicator"> <div ref={observerTarget} className="loading-indicator">
{isFetchingNextPage && <div className="loading-more"> ...</div>} {isLoadingMore && <div className="loading-more"> ...</div>}
</div> </div>
<SuitableButton /> <SuitableButton />
</> </>

View File

@ -1,16 +1,11 @@
'use client'
import { useRouter } from 'next/navigation'
export default function SuitableNoData() { export default function SuitableNoData() {
const router = useRouter()
return ( return (
<> <>
<div className="compliace-nosearch"> <div className="compliace-nosearch">
<span className="mb10"></span> <span className="mb10"></span>
<span className="mb10"> </span> <span className="mb10"> </span>
<span> <span>
<button className="btn-frame n-blue icon" onClick={() => router.push('/inquiry/regist')}> <button className="btn-frame n-blue icon">
<i className="btn-arr"></i> <i className="btn-arr"></i>
</button> </button>
</span> </span>

View File

@ -25,7 +25,7 @@ export default function BasicForm(props: { basicInfo: SurveyBasicRequest; setBas
setBasicInfo({ setBasicInfo({
...basicInfo, ...basicInfo,
representative: session.userNm ?? '', representative: session.userNm ?? '',
store: session.role === 'Partner' ? null : session.storeNm ?? null, store: session.storeNm ?? null,
constructionPoint: session.builderNo ?? null, constructionPoint: session.builderNo ?? null,
}) })
} }

View File

@ -22,15 +22,8 @@ export default function ButtonForm(props: {
const params = useParams() const params = useParams()
const routeId = params.id const routeId = params.id
const [isSubmitProcess, setIsSubmitProcess] = useState(false)
// ------------------------------------------------------------ // ------------------------------------------------------------
const [saveData, setSaveData] = useState({
...props.data.basic,
detailInfo: props.data.roof,
})
// !!!!!!!!!!
const [tempTargetId, setTempTargetId] = useState('')
// --------------------------------------------------------------
// 권한 // 권한
// 제출권한 ㅇ // 제출권한 ㅇ
@ -41,63 +34,39 @@ export default function ButtonForm(props: {
useEffect(() => { useEffect(() => {
if (session?.isLoggedIn) { if (session?.isLoggedIn) {
switch (session?.role) {
// T01 제출권한 없음
case 'T01':
setIsSubmiter(false)
break
// 1차 판매점(Order) + 2차 판매점(Musubi) => 같은 판매점 제출권한
case 'Admin':
case 'Admin_Sub':
setIsSubmiter(session.storeNm === props.data.basic.store && session.builderNo === props.data.basic.constructionPoint) setIsSubmiter(session.storeNm === props.data.basic.store && session.builderNo === props.data.basic.constructionPoint)
break
// 시공권한 User(Musubi) + Partner => 같은 시공ID 제출권한
case 'Builder':
case 'Partner':
setIsSubmiter(session.builderNo === props.data.basic.constructionPoint)
break
default:
setIsSubmiter(false)
break
}
setIsWriter(session.userNm === props.data.basic.representative) setIsWriter(session.userNm === props.data.basic.representative)
} }
setSaveData({
...props.data.basic,
detailInfo: props.data.roof,
})
}, [session, props.data]) }, [session, props.data])
// ------------------------------------------------------------ // ------------------------------------------------------------
// 저장/임시저장/수정 // 저장/임시저장/수정
const id = Number(routeId) ? Number(routeId) : Number(idParam)
const id = routeId ? Number(routeId) : Number(idParam)
const { deleteSurvey, submitSurvey, updateSurvey } = useServey(Number(id)) const { deleteSurvey, submitSurvey, updateSurvey } = useServey(Number(id))
const { validateSurveyDetail, createSurvey } = useServey() const { validateSurveyDetail, createSurvey } = useServey()
let saveData = {
...props.data.basic,
detailInfo: props.data.roof,
}
const handleSave = (isTemporary: boolean, isSubmitProcess = false) => { const handleSave = (isTemporary: boolean) => {
const emptyField = validateSurveyDetail(props.data.roof) const emptyField = validateSurveyDetail(props.data.roof)
const hasEmptyField = emptyField?.trim() !== '' console.log('handleSave, emptyField:: ', emptyField)
if (isTemporary) { if (isTemporary) {
hasEmptyField ? tempSaveProcess() : saveProcess(emptyField, false) tempSaveProcess()
} else { } else {
saveProcess(emptyField, isSubmitProcess) saveProcess(emptyField)
} }
} }
const tempSaveProcess = async () => { const tempSaveProcess = async () => {
if (idParam) { if (idParam) {
await updateSurvey({ survey: saveData, isTemporary: true }) await updateSurvey(saveData)
router.push(`/survey-sale/${idParam}`) router.push(`/survey-sale/detail?id=${idParam}&isTemporary=true`)
} else { } else {
const updatedData = { const id = await createSurvey(saveData)
...saveData, router.push(`/survey-sale/detail?id=${id}&isTemporary=true`)
srlNo: '一時保存',
}
const id = await createSurvey(updatedData)
router.push(`/survey-sale/${id}`)
} }
alert('一時保存されました。') alert('一時保存されました。')
} }
@ -109,40 +78,30 @@ export default function ButtonForm(props: {
} }
} }
const saveProcess = async (emptyField: string | null, isSubmitProcess?: boolean) => { const saveProcess = async (emptyField: string) => {
if (emptyField?.trim() === '') { if (emptyField.trim() === '') {
if (idParam) { if (idParam) {
// 수정 페이지에서 작성 후 제출
if (isSubmitProcess) { if (isSubmitProcess) {
const updatedData = { saveData = {
...saveData, ...saveData,
submissionStatus: true, submissionStatus: true,
submissionDate: new Date().toISOString(), submissionDate: new Date().toISOString(),
submissionTargetId: tempTargetId,
} }
await updateSurvey({ survey: updatedData, isTemporary: false, storeId: session.storeId ?? '' }) }
await updateSurvey(saveData)
router.push(`/survey-sale/${idParam}`) router.push(`/survey-sale/${idParam}`)
} else {
await updateSurvey({ survey: saveData, isTemporary: false, storeId: session.storeId ?? '' })
router.push(`/survey-sale/${idParam}`)
}
} else {
if (isSubmitProcess) {
const updatedData = {
...saveData,
submissionStatus: true,
submissionDate: new Date().toISOString(),
submissionTargetId: tempTargetId,
}
const id = await createSurvey(updatedData)
submitProcess(id)
} else { } else {
const id = await createSurvey(saveData) const id = await createSurvey(saveData)
router.push(`/survey-sale/${id}`) if (isSubmitProcess) {
submitProcess(id)
return
} }
router.push(`/survey-sale/${id}`)
} }
alert('保存されました。') alert('保存されました。')
} else { } else {
if (emptyField?.includes('Unit')) { if (emptyField.includes('Unit')) {
alert('電気契約容量の単位を入力してください。') alert('電気契約容量の単位を入力してください。')
focusInput(emptyField as keyof SurveyDetailInfo) focusInput(emptyField as keyof SurveyDetailInfo)
} else { } else {
@ -164,25 +123,17 @@ export default function ButtonForm(props: {
} }
const handleSubmit = async () => { const handleSubmit = async () => {
if (props.data.basic.srlNo?.startsWith('一時保存')) {
alert('一時保存されたデータは提出できません。')
return
}
if (tempTargetId.trim() === '') {
alert('提出対象店舗を入力してください。')
return
}
window.neoConfirm('提出しますか?', async () => { window.neoConfirm('提出しますか?', async () => {
if (Number(routeId)) { setIsSubmitProcess(true)
if (routeId) {
submitProcess() submitProcess()
} else { } else {
handleSave(false, true) handleSave(false)
} }
}) })
} }
const submitProcess = async (saveId?: number) => { const submitProcess = async (saveId?: number) => {
await submitSurvey({ saveId: saveId, targetId: tempTargetId, storeId: session.storeId ?? '', srlNo: '一時保存' }) await submitSurvey(saveId)
alert('提出されました。') alert('提出されました。')
router.push('/survey-sale') router.push('/survey-sale')
} }
@ -208,7 +159,7 @@ export default function ButtonForm(props: {
<ListButton /> <ListButton />
<EditButton setMode={setMode} id={id.toString()} mode={mode} /> <EditButton setMode={setMode} id={id.toString()} mode={mode} />
{(isWriter || !isSubmiter) && <DeleteButton handleDelete={handleDelete} />} {(isWriter || !isSubmiter) && <DeleteButton handleDelete={handleDelete} />}
{!isSubmit && isSubmiter && <SubmitButton handleSubmit={handleSubmit} setTempTargetId={setTempTargetId} />} {!isSubmit && isSubmiter && <SubmitButton handleSubmit={handleSubmit} />}
</div> </div>
</div> </div>
)} )}
@ -219,7 +170,7 @@ export default function ButtonForm(props: {
<ListButton /> <ListButton />
<TempButton setMode={setMode} handleSave={handleSave} /> <TempButton setMode={setMode} handleSave={handleSave} />
<SaveButton handleSave={handleSave} /> <SaveButton handleSave={handleSave} />
{session?.role !== 'T01' && <SubmitButton handleSubmit={handleSubmit} setTempTargetId={setTempTargetId} />}{' '} <SubmitButton handleSubmit={handleSubmit} />
</div> </div>
</div> </div>
)} )}
@ -259,20 +210,15 @@ function EditButton(props: { setMode: (mode: Mode) => void; id: string; mode: Mo
) )
} }
function SubmitButton(props: { handleSubmit: () => void; setTempTargetId: (targetId: string) => void }) { function SubmitButton(props: { handleSubmit: () => void }) {
const { handleSubmit, setTempTargetId } = props const { handleSubmit } = props
return ( return (
<>
<div className="btn-bx"> <div className="btn-bx">
{/* 제출 */} {/* 제출 */}
<button className="btn-frame red icon" onClick={handleSubmit}> <button className="btn-frame red icon" onClick={handleSubmit}>
<i className="btn-arr"></i> <i className="btn-arr"></i>
</button> </button>
</div> </div>
<div>
<input type="text" placeholder="temp target id" onChange={(e) => setTempTargetId(e.target.value)} />
</div>
</>
) )
} }
@ -310,6 +256,7 @@ function TempButton(props: { setMode: (mode: Mode) => void; handleSave: (isTempo
<button <button
className="btn-frame n-blue icon" className="btn-frame n-blue icon"
onClick={() => { onClick={() => {
setMode('TEMP')
handleSave(true) handleSave(true)
}} }}
> >

View File

@ -1,22 +1,31 @@
'use client' 'use client'
import { useServey } from '@/hooks/useSurvey' import { useServey } from '@/hooks/useSurvey'
import { useParams } from 'next/navigation' import { useParams, useSearchParams } from 'next/navigation'
import { useEffect } from 'react' import { useEffect, useState } from 'react'
import DetailForm from './DetailForm' import DetailForm from './DetailForm'
import type { SurveyBasicInfo } from '@/types/Survey'
export default function DataTable() { export default function DataTable() {
const params = useParams() const params = useParams()
const id = params.id const id = params.id
useEffect(() => { const searchParams = useSearchParams()
if (Number.isNaN(Number(id))) { const isTemp = searchParams.get('isTemporary')
alert('間違ったアプローチです。')
window.location.href = '/survey-sale'
}
}, [id])
const { surveyDetail, isLoadingSurveyDetail } = useServey(Number(id)) const { surveyDetail, isLoadingSurveyDetail } = useServey(Number(id))
const [isTemporary, setIsTemporary] = useState(isTemp === 'true')
const { validateSurveyDetail } = useServey(Number(id))
useEffect(() => {
if (surveyDetail?.detailInfo) {
const validate = validateSurveyDetail(surveyDetail.detailInfo)
if (validate.trim() !== '') {
setIsTemporary(false)
}
}
}, [surveyDetail])
if (isLoadingSurveyDetail) { if (isLoadingSurveyDetail) {
return <div>Loading...</div> return <div>Loading...</div>
@ -33,12 +42,12 @@ export default function DataTable() {
<tbody> <tbody>
<tr> <tr>
<th></th> <th></th>
{surveyDetail?.srlNo?.startsWith('一時保存') ? ( {isTemporary ? (
<td> <td>
<span className="text-red-500"></span> <span className="text-red-500"></span>
</td> </td>
) : ( ) : (
<td>{surveyDetail?.srlNo}</td> <td>{surveyDetail?.id}</td>
)} )}
</tr> </tr>
<tr> <tr>

View File

@ -5,7 +5,7 @@ import { useEffect, useState } from 'react'
import ButtonForm from './ButtonForm' import ButtonForm from './ButtonForm'
import BasicForm from './BasicForm' import BasicForm from './BasicForm'
import RoofForm from './RoofForm' import RoofForm from './RoofForm'
import { useParams, useRouter, useSearchParams } from 'next/navigation' import { useParams, useSearchParams } from 'next/navigation'
import { useServey } from '@/hooks/useSurvey' import { useServey } from '@/hooks/useSurvey'
const roofInfoForm: SurveyDetailRequest = { const roofInfoForm: SurveyDetailRequest = {
@ -58,40 +58,34 @@ const basicInfoForm: SurveyBasicRequest = {
addressDetail: null, addressDetail: null,
submissionStatus: false, submissionStatus: false,
submissionDate: null, submissionDate: null,
submissionTargetId: null,
srlNo: null,
} }
export default function DetailForm() { export default function DetailForm() {
const idParam = useSearchParams().get('id') const idParam = useSearchParams().get('id')
const routeId = useParams().id const routeId = useParams().id
const modeset = Number(routeId) ? 'READ' : idParam ? 'EDIT' : 'CREATE' const id = idParam ?? routeId
const id = Number(routeId) ? Number(routeId) : Number(idParam)
const { surveyDetail, validateSurveyDetail } = useServey(Number(id)) const { surveyDetail } = useServey(Number(id))
const [mode, setMode] = useState<Mode>(modeset) const [mode, setMode] = useState<Mode>(idParam ? 'EDIT' : routeId ? 'READ' : 'CREATE')
const [basicInfoData, setBasicInfoData] = useState<SurveyBasicRequest>(basicInfoForm) const [basicInfoData, setBasicInfoData] = useState<SurveyBasicRequest>(basicInfoForm)
const [roofInfoData, setRoofInfoData] = useState<SurveyDetailRequest>(roofInfoForm) const [roofInfoData, setRoofInfoData] = useState<SurveyDetailRequest>(roofInfoForm)
useEffect(() => { useEffect(() => {
if (Number(idParam) !== 0 && surveyDetail === null) {
alert('データが見つかりません。')
window.location.href = '/survey-sale'
}
if (surveyDetail && (mode === 'EDIT' || mode === 'READ')) { if (surveyDetail && (mode === 'EDIT' || mode === 'READ')) {
const { id, uptDt, regDt, detailInfo, ...rest } = surveyDetail const { id, uptDt, regDt, detailInfo, ...rest } = surveyDetail
setBasicInfoData(rest) setBasicInfoData(rest)
if (detailInfo) { if (detailInfo) {
const { id, uptDt, regDt, basicInfoId, ...rest } = detailInfo const { id, uptDt, regDt, basicInfoId, ...rest } = detailInfo
setRoofInfoData(rest) setRoofInfoData(rest)
if (validateSurveyDetail(rest).trim() !== '') {
} }
} }
} }, [surveyDetail, mode])
}, [surveyDetail, id])
// console.log('mode:: ', mode)
// console.log('surveyDetail:: ', surveyDetail)
// console.log('roofInfoData:: ', roofInfoData)
const data = { const data = {
basic: basicInfoData, basic: basicInfoData,

View File

@ -230,14 +230,6 @@ export default function RoofForm(props: {
} }
} }
} }
if (key === 'contractCapacity') {
const remainValue = roofInfo.contractCapacity?.split(' ')[1] ?? roofInfo.contractCapacity
if (Number.isNaN(Number(remainValue))) {
setRoofInfo({ ...roofInfo, [key]: value + ' ' + remainValue })
return
}
setRoofInfo({ ...roofInfo, [key]: value.toString() })
}
setRoofInfo({ ...roofInfo, [key]: value.toString() }) setRoofInfo({ ...roofInfo, [key]: value.toString() })
} }
@ -245,7 +237,7 @@ export default function RoofForm(props: {
const numericValue = roofInfo.contractCapacity?.replace(/[^0-9.]/g, '') || '' const numericValue = roofInfo.contractCapacity?.replace(/[^0-9.]/g, '') || ''
setRoofInfo({ setRoofInfo({
...roofInfo, ...roofInfo,
contractCapacity: numericValue ? `${numericValue} ${value}` : '0 ' + value, contractCapacity: numericValue ? `${numericValue} ${value}` : value,
}) })
} }
@ -269,7 +261,7 @@ export default function RoofForm(props: {
{mode !== 'READ' && ( {mode !== 'READ' && (
<div className="data-input mb5"> <div className="data-input mb5">
<input <input
type="number" type="text"
id="contractCapacity" id="contractCapacity"
className="input-frame" className="input-frame"
value={roofInfo?.contractCapacity?.split(' ')[0] ?? ''} value={roofInfo?.contractCapacity?.split(' ')[0] ?? ''}
@ -472,17 +464,17 @@ const SelectedBox = ({
}) => { }) => {
const selectedId = detailInfoData?.[column as keyof SurveyDetailInfo] const selectedId = detailInfoData?.[column as keyof SurveyDetailInfo]
const etcValue = detailInfoData?.[`${column}Etc` as keyof SurveyDetailInfo] const etcValue = detailInfoData?.[`${column}Etc` as keyof SurveyDetailInfo]
const [isEtcSelected, setIsEtcSelected] = useState<boolean>(Boolean(etcValue))
const isSpecialCase = column === 'constructionYear' || column === 'installationAvailability' const [isEtcSelected, setIsEtcSelected] = useState<boolean>(etcValue !== null && etcValue !== undefined && etcValue !== '')
const showEtcOption = !isSpecialCase const [etcVal, setEtcVal] = useState<string>(etcValue?.toString() ?? '')
const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => { const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value const value = e.target.value
const isSpecialCase = column === 'constructionYear' || column === 'installationAvailability'
const isEtc = value === 'etc' const isEtc = value === 'etc'
const isSpecialEtc = isSpecialCase && value === '2' const isSpecialEtc = isSpecialCase && value === '2'
const updatedData = { const updatedData: typeof detailInfoData = {
...detailInfoData, ...detailInfoData,
[column]: isEtc ? null : value, [column]: isEtc ? null : value,
[`${column}Etc`]: isEtc ? '' : null, [`${column}Etc`]: isEtc ? '' : null,
@ -493,20 +485,14 @@ const SelectedBox = ({
} }
setIsEtcSelected(isEtc || isSpecialEtc) setIsEtcSelected(isEtc || isSpecialEtc)
if (!isEtc) setEtcVal('')
setRoofInfo(updatedData) setRoofInfo(updatedData)
} }
const handleEtcInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleEtcInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setRoofInfo({ ...detailInfoData, [`${column}Etc`]: e.target.value }) const value = e.target.value
} setEtcVal(value)
setRoofInfo({ ...detailInfoData, [`${column}Etc`]: value })
const isInputDisabled = () => {
if (mode === 'READ') return true
if (column === 'installationAvailability') return false
if (column === 'constructionYear') {
return detailInfoData.constructionYear === '1' || detailInfoData.constructionYear === null
}
return !isEtcSelected && !etcValue
} }
return ( return (
@ -516,7 +502,7 @@ const SelectedBox = ({
name={column} name={column}
id={column} id={column}
disabled={mode === 'READ'} disabled={mode === 'READ'}
value={selectedId ? Number(selectedId) : etcValue ? 'etc' : ''} value={selectedId ? Number(selectedId) : etcValue !== null ? 'etc' : ''}
onChange={handleSelectChange} onChange={handleSelectChange}
> >
{selectBoxOptions[column as keyof typeof selectBoxOptions].map((item) => ( {selectBoxOptions[column as keyof typeof selectBoxOptions].map((item) => (
@ -524,7 +510,7 @@ const SelectedBox = ({
{item.name} {item.name}
</option> </option>
))} ))}
{showEtcOption && ( {column !== 'installationAvailability' && column !== 'constructionYear' && (
<option key="etc" value="etc"> <option key="etc" value="etc">
() ()
</option> </option>
@ -533,16 +519,23 @@ const SelectedBox = ({
</option> </option>
</select> </select>
<div className={`data-input ${column === 'constructionYear' ? 'flex' : ''}`}> <div className="data-input">
<input <input
type={column === 'constructionYear' ? 'number' : 'text'} type="text"
className="input-frame" className="input-frame"
placeholder="-" placeholder="-"
value={detailInfoData[`${column}Etc` as keyof SurveyDetailInfo]?.toString() ?? ''} value={etcVal}
onChange={handleEtcInputChange} onChange={handleEtcInputChange}
disabled={isInputDisabled()} disabled={
mode === 'READ'
? true
: column === 'installationAvailability'
? false
: column === 'constructionYear'
? detailInfoData.constructionYear === '1' || detailInfoData.constructionYear === null
: !isEtcSelected
}
/> />
{column === 'constructionYear' && <span></span>}
</div> </div>
</> </>
) )
@ -559,51 +552,49 @@ const RadioSelected = ({
detailInfoData: SurveyDetailInfo detailInfoData: SurveyDetailInfo
setRoofInfo: (roofInfo: SurveyDetailRequest) => void setRoofInfo: (roofInfo: SurveyDetailRequest) => void
}) => { }) => {
const etcValue = detailInfoData?.[`${column}Etc` as keyof SurveyDetailInfo] let selectedId = detailInfoData?.[column as keyof SurveyDetailInfo]
const [etcChecked, setEtcChecked] = useState<boolean>(Boolean(etcValue)) if (column === 'leakTrace') {
selectedId = Number(selectedId)
if (!selectedId) selectedId = 2
}
const selectedId = let etcValue = null
column === 'leakTrace' ? Number(detailInfoData?.[column as keyof SurveyDetailInfo]) || 2 : detailInfoData?.[column as keyof SurveyDetailInfo] if (column !== 'rafterDirection') {
etcValue = detailInfoData?.[`${column}Etc` as keyof SurveyDetailInfo]
const isSpecialColumn = column === 'rafterDirection' || column === 'leakTrace' || column === 'insulationPresence' }
const showEtcOption = !isSpecialColumn const [etcChecked, setEtcChecked] = useState<boolean>(etcValue !== null && etcValue !== undefined && etcValue !== '')
const [etcVal, setEtcVal] = useState<string>(etcValue?.toString() ?? '')
const handleRadioChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleRadioChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value const value = e.target.value
if (column === 'leakTrace') { if (column === 'leakTrace') {
setRoofInfo({ ...detailInfoData, leakTrace: value === '1' }) handleBooleanRadioChange(value)
return
} }
if (value === 'etc') { if (value === 'etc') {
setEtcChecked(true) setEtcChecked(true)
setRoofInfo({ ...detailInfoData, [column]: null, [`${column}Etc`]: '' }) setRoofInfo({ ...detailInfoData, [column]: null, [`${column}Etc`]: '' })
return } else {
if (column === 'insulationPresence' && value === '2') {
setEtcChecked(true)
} else {
setEtcChecked(false)
}
setRoofInfo({ ...detailInfoData, [column]: value, [`${column}Etc`]: null })
}
} }
const isInsulationPresence = column === 'insulationPresence' const handleBooleanRadioChange = (value: string) => {
const isRafterDirection = column === 'rafterDirection' if (value === '1') {
setRoofInfo({ ...detailInfoData, leakTrace: true })
setEtcChecked(isInsulationPresence && value === '2') } else {
setRoofInfo({ ...detailInfoData, leakTrace: false })
setRoofInfo({ }
...detailInfoData,
[column]: value,
[`${column}Etc`]: isRafterDirection ? detailInfoData[`${column}Etc` as keyof SurveyDetailInfo] : null,
})
} }
const handleEtcInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleEtcInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setRoofInfo({ ...detailInfoData, [`${column}Etc`]: e.target.value }) const value = e.target.value
} setEtcVal(value)
setRoofInfo({ ...detailInfoData, [`${column}Etc`]: value })
const isInputDisabled = () => {
if (mode === 'READ') return true
if (column === 'insulationPresence') {
return detailInfoData.insulationPresence !== '2'
}
return !etcChecked && !etcValue
} }
return ( return (
@ -622,7 +613,7 @@ const RadioSelected = ({
<label htmlFor={`${column}_${item.id}`}>{item.label}</label> <label htmlFor={`${column}_${item.id}`}>{item.label}</label>
</div> </div>
))} ))}
{showEtcOption && ( {column !== 'rafterDirection' && column !== 'leakTrace' && column !== 'insulationPresence' && (
<div className="radio-form-box mb10"> <div className="radio-form-box mb10">
<input <input
type="radio" type="radio"
@ -630,21 +621,21 @@ const RadioSelected = ({
id={`${column}Etc`} id={`${column}Etc`}
value="etc" value="etc"
disabled={mode === 'READ'} disabled={mode === 'READ'}
checked={etcChecked || Boolean(etcValue)} checked={etcChecked}
onChange={handleRadioChange} onChange={handleRadioChange}
/> />
<label htmlFor={`${column}Etc`}> ()</label> <label htmlFor={`${column}Etc`}> ()</label>
</div> </div>
)} )}
{(showEtcOption || column === 'insulationPresence') && ( {column !== 'leakTrace' && column !== 'rafterDirection' && (
<div className="data-input"> <div className="data-input">
<input <input
type="text" type="text"
className="input-frame" className="input-frame"
placeholder="-" placeholder="-"
value={detailInfoData[`${column}Etc` as keyof SurveyDetailInfo]?.toString() ?? ''} value={etcVal}
onChange={handleEtcInputChange} onChange={handleEtcInputChange}
disabled={isInputDisabled()} disabled={mode === 'READ' || !etcChecked}
/> />
</div> </div>
)} )}
@ -664,56 +655,51 @@ const MultiCheck = ({
setRoofInfo: (roofInfo: SurveyDetailRequest) => void setRoofInfo: (roofInfo: SurveyDetailRequest) => void
}) => { }) => {
const multiCheckData = column === 'supplementaryFacilities' ? supplementaryFacilities : roofMaterial const multiCheckData = column === 'supplementaryFacilities' ? supplementaryFacilities : roofMaterial
const etcValue = roofInfo?.[`${column}Etc` as keyof SurveyDetailInfo]
const [isOtherCheck, setIsOtherCheck] = useState<boolean>(Boolean(etcValue))
const isRoofMaterial = column === 'roofMaterial' const [isOtherCheck, setIsOtherCheck] = useState<boolean>(false)
const selectedValues = makeNumArr(String(roofInfo[column as keyof SurveyDetailInfo] ?? '')) const [otherValue, setOtherValue] = useState<string>(roofInfo?.[`${column}Etc` as keyof SurveyDetailInfo]?.toString() ?? '')
const handleCheckbox = (id: number) => { const handleCheckbox = (id: number) => {
const isOtherSelected = Boolean(etcValue) const value = makeNumArr(String(roofInfo[column as keyof SurveyDetailInfo] ?? ''))
let newValue: string[] const isOtherSelected = roofInfo?.[`${column}Etc` as keyof SurveyDetailInfo] !== null
if (selectedValues.includes(String(id))) { let newValue: string[]
newValue = selectedValues.filter((v) => v !== String(id)) if (value.includes(String(id))) {
newValue = value.filter((v) => v !== String(id))
} else { } else {
if (isRoofMaterial) { if (column === 'roofMaterial') {
const totalSelected = selectedValues.length + (isOtherSelected ? 1 : 0) const totalSelected = value.length + (isOtherSelected ? 1 : 0)
if (totalSelected >= 2) { if (totalSelected >= 2) {
alert('屋根材は最大2個まで選択できます。') alert('屋根材は最大2個まで選択できます。')
return return
} }
} }
newValue = [...selectedValues, String(id)] newValue = [...value, String(id)]
} }
setRoofInfo({ ...roofInfo, [column]: newValue.join(',') }) setRoofInfo({ ...roofInfo, [column]: newValue.join(',') })
} }
const handleOtherCheckbox = () => { const handleOtherCheckbox = () => {
if (isRoofMaterial) { if (column === 'roofMaterial') {
const currentSelected = selectedValues.length const value = makeNumArr(String(roofInfo[column as keyof SurveyDetailInfo] ?? ''))
const currentSelected = value.length
if (!isOtherCheck && currentSelected >= 2) { if (!isOtherCheck && currentSelected >= 2) {
alert('屋根材は最大2個まで選択できます。') alert('屋根材は最大2個まで選択できます。')
return return
} }
} }
const newIsOtherCheck = !isOtherCheck const newIsOtherCheck = !isOtherCheck
setIsOtherCheck(newIsOtherCheck) setIsOtherCheck(newIsOtherCheck)
setOtherValue('')
// 기타 선택 해제 시 값도 null로 설정 setRoofInfo({ ...roofInfo, [`${column}Etc`]: newIsOtherCheck ? '' : null })
setRoofInfo({
...roofInfo,
[`${column}Etc`]: newIsOtherCheck ? '' : null,
})
} }
const handleOtherInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleOtherInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setRoofInfo({ ...roofInfo, [`${column}Etc`]: e.target.value }) const value = e.target.value
} setOtherValue(value)
setRoofInfo({ ...roofInfo, [`${column}Etc`]: value })
const isInputDisabled = () => {
return mode === 'READ' || (!isOtherCheck && !etcValue)
} }
return ( return (
@ -724,7 +710,7 @@ const MultiCheck = ({
<input <input
type="checkbox" type="checkbox"
id={`${column}_${item.id}`} id={`${column}_${item.id}`}
checked={selectedValues.includes(String(item.id))} checked={makeNumArr(String(roofInfo[column as keyof SurveyDetailInfo] ?? '')).includes(String(item.id))}
disabled={mode === 'READ'} disabled={mode === 'READ'}
onChange={() => handleCheckbox(item.id)} onChange={() => handleCheckbox(item.id)}
/> />
@ -735,7 +721,7 @@ const MultiCheck = ({
<input <input
type="checkbox" type="checkbox"
id={`${column}Etc`} id={`${column}Etc`}
checked={isOtherCheck || Boolean(etcValue)} checked={roofInfo?.[`${column}Etc` as keyof SurveyDetailInfo] !== null}
disabled={mode === 'READ'} disabled={mode === 'READ'}
onChange={handleOtherCheckbox} onChange={handleOtherCheckbox}
/> />
@ -747,9 +733,9 @@ const MultiCheck = ({
type="text" type="text"
className="input-frame" className="input-frame"
placeholder="-" placeholder="-"
value={roofInfo[`${column}Etc` as keyof SurveyDetailInfo]?.toString() ?? ''} value={otherValue}
onChange={handleOtherInputChange} onChange={handleOtherInputChange}
disabled={isInputDisabled()} disabled={mode === 'READ' || !isOtherCheck}
/> />
</div> </div>
</> </>

View File

@ -2,8 +2,8 @@
import LoadMoreButton from '@/components/LoadMoreButton' import LoadMoreButton from '@/components/LoadMoreButton'
import { useServey } from '@/hooks/useSurvey' import { useServey } from '@/hooks/useSurvey'
import { useEffect, useState, useMemo, useRef } from 'react' import { useEffect, useState } from 'react'
import { useRouter, usePathname } from 'next/navigation' import { useRouter } from 'next/navigation'
import SearchForm from './SearchForm' import SearchForm from './SearchForm'
import { useSurveyFilterStore } from '@/store/surveyFilterStore' import { useSurveyFilterStore } from '@/store/surveyFilterStore'
import { useSessionStore } from '@/store/session' import { useSessionStore } from '@/store/session'
@ -11,20 +11,13 @@ import type { SurveyBasicInfo } from '@/types/Survey'
export default function ListTable() { export default function ListTable() {
const router = useRouter() const router = useRouter()
const pathname = usePathname()
const { surveyList, isLoadingSurveyList } = useServey() const { surveyList, isLoadingSurveyList } = useServey()
const { offset, setOffset } = useSurveyFilterStore() const { offset, setOffset } = useSurveyFilterStore()
const { session } = useSessionStore()
const [heldSurveyList, setHeldSurveyList] = useState<SurveyBasicInfo[]>([]) const [heldSurveyList, setHeldSurveyList] = useState<SurveyBasicInfo[]>([])
const [hasMore, setHasMore] = useState(false) const [hasMore, setHasMore] = useState(false)
useEffect(() => { const { session } = useSessionStore()
setOffset(0)
setHeldSurveyList([])
}, [pathname])
useEffect(() => { useEffect(() => {
if (!session.isLoggedIn || !('data' in surveyList)) return if (!session.isLoggedIn || !('data' in surveyList)) return
@ -39,25 +32,30 @@ export default function ListTable() {
setHeldSurveyList([]) setHeldSurveyList([])
setHasMore(false) setHasMore(false)
} }
}, [surveyList, offset, session.isLoggedIn]) }, [surveyList, offset, session])
const handleDetailClick = (id: number) => { const handleDetailClick = (id: number) => {
router.push(`/survey-sale/${id}`) router.push(`/survey-sale/${id}`)
} }
const handleItemsInit = () => {
setHeldSurveyList([])
setOffset(0)
}
// TODO: 로딩 처리 필요 // TODO: 로딩 처리 필요
return ( return (
<> <>
<SearchForm memberRole={session?.role ?? ''} userNm={session?.userNm ?? ''} /> <SearchForm memberRole={session?.role ?? ''} userId={session?.userId ?? ''} />
<div className="sale-frame">
{heldSurveyList.length > 0 ? ( {heldSurveyList.length > 0 ? (
<div className="sale-frame">
<ul className="sale-list-wrap"> <ul className="sale-list-wrap">
{heldSurveyList.map((survey) => ( {heldSurveyList.map((survey) => (
<li className="sale-list-item cursor-pointer" key={survey.id} onClick={() => handleDetailClick(survey.id)}> <li className="sale-list-item cursor-pointer" key={survey.id} onClick={() => handleDetailClick(survey.id)}>
<div className="sale-item-bx"> <div className="sale-item-bx">
<div className="sale-item-date-bx"> <div className="sale-item-date-bx">
<div className="sale-item-num">{survey.srlNo}</div> <div className="sale-item-num">{survey.id}</div>
<div className="sale-item-date">{survey.investigationDate}</div> <div className="sale-item-date">{survey.investigationDate}</div>
</div> </div>
<div className="sale-item-tit">{survey.buildingName}</div> <div className="sale-item-tit">{survey.buildingName}</div>
@ -70,15 +68,15 @@ export default function ListTable() {
</li> </li>
))} ))}
</ul> </ul>
) : (
<div className="compliace-nosearch">
<span className="mb10"></span>
</div>
)}
<div className="sale-edit-btn"> <div className="sale-edit-btn">
<LoadMoreButton hasMore={hasMore} onLoadMore={() => setOffset(offset + 10)} /> <LoadMoreButton hasMore={hasMore} onLoadMore={() => setOffset(offset + 10)} />
</div> </div>
</div> </div>
) : (
<div>
<p></p>
</div>
)}
</> </>
) )
} }

View File

@ -4,7 +4,7 @@ import { SEARCH_OPTIONS, SEARCH_OPTIONS_ENUM, SEARCH_OPTIONS_PARTNERS, useSurvey
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useState } from 'react' import { useState } from 'react'
export default function SearchForm({ memberRole, userNm }: { memberRole: string; userNm: string }) { export default function SearchForm({ memberRole, userId }: { memberRole: string; userId: string }) {
const router = useRouter() const router = useRouter()
const { setSearchOption, setSort, setIsMySurvey, setKeyword, isMySurvey, keyword, searchOption, sort } = useSurveyFilterStore() const { setSearchOption, setSort, setIsMySurvey, setKeyword, isMySurvey, keyword, searchOption, sort } = useSurveyFilterStore()
const [searchKeyword, setSearchKeyword] = useState(keyword) const [searchKeyword, setSearchKeyword] = useState(keyword)
@ -75,9 +75,9 @@ export default function SearchForm({ memberRole, userNm }: { memberRole: string;
<input <input
type="checkbox" type="checkbox"
id="ch01" id="ch01"
checked={isMySurvey === userNm} checked={isMySurvey === userId}
onChange={() => { onChange={() => {
setIsMySurvey(isMySurvey === userNm ? null : userNm) setIsMySurvey(isMySurvey === userId ? null : userId)
}} }}
/> />
<label htmlFor="ch01"></label> <label htmlFor="ch01"></label>

View File

@ -34,7 +34,7 @@ export default function Main() {
<div className="main-bx-icon"> <div className="main-bx-icon">
<img src="/assets/images/main/main_icon02.svg" alt="" /> <img src="/assets/images/main/main_icon02.svg" alt="" />
</div> </div>
<button className="main-bx-arr" onClick={() => router.push('/survey-sale/regist')}></button> <button className="main-bx-arr" onClick={() => router.push('/survey-sale/basic-info')}></button>
</div> </div>
<div className="grid-bx-body"> <div className="grid-bx-body">
<div className="grid-bx-body-tit">調</div> <div className="grid-bx-body-tit">調</div>

View File

@ -1,7 +1,6 @@
'use client' 'use client'
import Link from 'next/link' import Link from 'next/link'
import Config from '@/config/config.export'
export default function Footer() { export default function Footer() {
return ( return (
@ -12,9 +11,6 @@ export default function Footer() {
<span> <span>
<Link href="/pdf">PDF</Link> <Link href="/pdf">PDF</Link>
</span> </span>
<span>{Config().mode}</span>
<span>{Config().baseUrl}</span>
<span>{process.env.NEXT_PUBLIC_API_URL}</span>
</div> </div>
</footer> </footer>
</> </>

View File

@ -13,13 +13,14 @@ import { useSessionStore } from '@/store/session'
import { usePopupController } from '@/store/popupController' import { usePopupController } from '@/store/popupController'
import { useTitle } from '@/hooks/useTitle' import { useTitle } from '@/hooks/useTitle'
import { useAxios } from '@/hooks/useAxios'
import { axiosInstance } from '@/libs/axios'
import 'swiper/css' import 'swiper/css'
export default function Header() { export default function Header() {
const router = useRouter() const router = useRouter()
const pathname = usePathname() const pathname = usePathname()
const { axiosInstance } = useAxios()
const [value, setValue, removeValue] = useLocalStorage<{ indivisualData: string }>('hanasysIndivisualState', { indivisualData: '' }) const [value, setValue, removeValue] = useLocalStorage<{ indivisualData: string }>('hanasysIndivisualState', { indivisualData: '' })
const { sideNavIsOpen, setSideNavIsOpen } = useSideNavState() const { sideNavIsOpen, setSideNavIsOpen } = useSideNavState()
const { backBtn } = useHeaderStore() const { backBtn } = useHeaderStore()

View File

@ -1,7 +0,0 @@
export default function Spinner() {
return (
<div className="spinner-wrap">
<span className="loader"></span>
</div>
)
}

View File

@ -1,20 +0,0 @@
export declare namespace ICommonConfig {
export type Mode = 'local' | 'development' | 'production'
export interface Params {
baseUrl: string
mode: Mode
}
}
// local, development, production 과 관계없이 동일한 값으로 반환되는 부분은 해당 함수의 return 되는 부분만 수정하면 됩니다. (달라져야 하는 값이 아닌, 같은 값에 대해서는 local, development, production 파일을 모두 수정할 필요가 없어지게 됩니다.)
export default function getConfigs(params: ICommonConfig.Params) {
// local, development, production 마다 달라지는 값
const { baseUrl, mode } = params
// 공통으로 반환되는 구조
return {
baseUrl,
mode,
}
}

View File

@ -1,13 +0,0 @@
import getConfigs from '@/config/config.common'
// 환경마다 달라져야 할 변수, 값들을 정의합니다. (여기는 development 환경에 맞는 값을 지정합니다.)
const baseUrl = 'http://1.248.227.176:3000'
const mode = 'development'
// 환경마다 달라져야 할 값들을 getConfig 함수에 전달합니다.
const configDevelopment = getConfigs({
baseUrl,
mode,
})
export default configDevelopment

View File

@ -1,19 +0,0 @@
import configDevelopment from './config.development'
import configLocal from './config.local'
import configProduction from './config.production'
// 클라이언트에서는 이 함수를 사용하여 config 값을 참조합니다.
const Config = () => {
switch (process.env.NEXT_PUBLIC_RUN_MODE) {
case 'local':
return configLocal
case 'development':
return configDevelopment
case 'production':
return configProduction
default:
return configLocal
}
}
export default Config

View File

@ -1,13 +0,0 @@
import getConfigs from '@/config/config.common'
// 환경마다 달라져야 할 변수, 값들을 정의합니다. (여기는 local 환경에 맞는 값을 지정합니다.)
const baseUrl = 'http://localhost:3000'
const mode = 'local'
// 환경마다 달라져야 할 값들을 getConfig 함수에 전달합니다.
const configLocal = getConfigs({
baseUrl,
mode,
})
export default configLocal

View File

@ -1,13 +0,0 @@
import getConfigs from '@/config/config.common'
// 환경마다 달라져야 할 변수, 값들을 정의합니다. (여기는 production 환경에 맞는 값을 지정합니다.)
const baseUrl = 'http://localhost.prod:3000'
const mode = 'production'
// 환경마다 달라져야 할 값들을 getConfig 함수에 전달합니다.
const configProduction = getConfigs({
baseUrl,
mode,
})
export default configProduction

View File

@ -1,119 +0,0 @@
import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios'
import Config from '@/config/config.export'
import { useSpinnerStore } from '@/store/spinnerStore'
export function useAxios() {
// const { setIsShow } = useSpinnerStore()
const requestHandler = (config: InternalAxiosRequestConfig) => {
// setIsShow(true)
return config
}
const responseHandler = (response: AxiosResponse) => {
// setIsShow(false)
response.data = transferResponse(response)
return response
}
const errorHandler = (error: any) => {
// setIsShow(false)
return Promise.reject(error)
}
const createAxiosInstance = (url: string | null | undefined) => {
const baseURL = url || Config().baseUrl
return axios.create({
baseURL,
headers: {
Accept: 'application/json',
},
})
}
const axiosInstance = (url: string | null | undefined) => {
const instance = axios.create({
baseURL: url || Config().baseUrl,
headers: {
Accept: 'application/json',
},
})
instance.interceptors.request.use(
// (config) => {
// return config
// },
// (error) => {
// return Promise.reject(error)
// },
(config) => requestHandler(config),
(error) => errorHandler(error),
)
instance.interceptors.response.use(
// (response) => {
// response.data = transferResponse(response)
// return response
// },
// (error) => {
// return Promise.reject(error)
// },
(response) => responseHandler(response),
(error) => errorHandler(error),
)
return instance
}
// response데이터가 array, object에 따라 분기하여 키 변환
const transferResponse = (response: any) => {
if (!response.data) return response.data
// 배열인 경우 각 객체의 키를 변환
if (Array.isArray(response.data)) {
return response.data.map((item: any) => transformObjectKeys(item))
}
// 단일 객체인 경우
return transformObjectKeys(response.data)
}
// camel case object 반환
const transformObjectKeys = (obj: any): any => {
if (Array.isArray(obj)) {
return obj.map(transformObjectKeys)
}
if (obj !== null && typeof obj === 'object') {
return Object.keys(obj).reduce((acc: any, key: string) => {
let transformedKey = key
// Handle uppercase snake_case (e.g., USER_NAME -> userName)
// Handle lowercase snake_case (e.g., user_name -> userName)
if (/^[A-Z_]+$/.test(key) || /^[a-z_]+$/.test(key)) {
transformedKey = snakeToCamel(key)
}
// Handle single uppercase word (e.g., ROLE -> role)
else if (/^[A-Z]+$/.test(key)) {
transformedKey = key.toLowerCase()
}
// Preserve existing camelCase
acc[transformedKey] = transformObjectKeys(obj[key])
return acc
}, {})
}
return obj
}
const snakeToCamel = (str: string): string => {
return str.toLowerCase().replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace('-', '').replace('_', ''))
}
return {
axiosInstance,
transferResponse,
transformObjectKeys,
}
}

View File

@ -1,37 +1,24 @@
import { useInfiniteQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { transformObjectKeys } from '@/libs/axios' import { axiosInstance, transformObjectKeys } from '@/libs/axios'
import { useSuitableStore } from '@/store/useSuitableStore' import { useSuitableStore } from '@/store/useSuitableStore'
import { useAxios } from './useAxios'
import { useCommCode } from './useCommCode' import { useCommCode } from './useCommCode'
import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/Suitable' import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/Suitable'
export function useSuitable() { export function useSuitable() {
const { axiosInstance } = useAxios()
const { getCommCode } = useCommCode() const { getCommCode } = useCommCode()
const { itemPerPage, selectedCategory, searchValue, suitableCommCode, setSuitableCommCode, isSearch } = useSuitableStore() const { selectedCategory, searchValue, suitableCommCode, setSuitableCommCode, isSearch } = useSuitableStore()
const getSuitables = async (): Promise<Suitable[]> => {
const getSuitables = async ({
pageNumber,
ids,
category,
keyword,
}: {
pageNumber?: number
ids?: string
category?: string
keyword?: string
}): Promise<Suitable[]> => {
try { try {
const params: Record<string, string | number> = { const response = await axiosInstance(null).get<Suitable[]>('/api/suitable/list', {
pageNumber: pageNumber || 1, params: {
itemPerPage: itemPerPage, pageNumber: 1,
} itemPerPage: 1000,
if (ids) params.ids = ids ids: '',
if (category) params.category = category category: '',
if (keyword) params.keyword = keyword keyword: '',
},
const response = await axiosInstance(null).get<Suitable[]>('/api/suitable/list', { params }) })
return response.data return response.data
} catch (error) { } catch (error) {
console.error('지붕재 데이터 로드 실패:', error) console.error('지붕재 데이터 로드 실패:', error)
@ -39,6 +26,16 @@ export function useSuitable() {
} }
} }
// const updateSearchResults = async (selectedCategory: string | undefined, searchValue: string | undefined): Promise<SuitableData[]> => {
// try {
// const response = await axiosInstance(null).get<SuitableData[]>('/api/suitable/list', { params: { selectedCategory, searchValue } })
// return response.data
// } catch (error) {
// console.error('지붕재 데이터 검색 실패:', error)
// return []
// }
// }
const getSuitableCommCode = () => { const getSuitableCommCode = () => {
const headCodes = Object.values(SUITABLE_HEAD_CODE) as SUITABLE_HEAD_CODE[] const headCodes = Object.values(SUITABLE_HEAD_CODE) as SUITABLE_HEAD_CODE[]
for (const code of headCodes) { for (const code of headCodes) {
@ -66,30 +63,35 @@ export function useSuitable() {
} }
} }
const { const { data: suitableList, isLoading: isInitialLoading } = useQuery<Suitable[]>({
data: suitables, queryKey: ['suitables', 'list'],
fetchNextPage, queryFn: async () => await getSuitables(),
hasNextPage, staleTime: 1000 * 60 * 10, // 10분
isFetchingNextPage, gcTime: 1000 * 60 * 10, // 10분
isLoading,
isError,
error,
} = useInfiniteQuery<Suitable[]>({
queryKey: ['suitables', 'list', selectedCategory, isSearch],
queryFn: async (context) => {
const pageParam = context.pageParam as number
return await getSuitables({
pageNumber: pageParam,
...(selectedCategory && { category: selectedCategory }),
...(isSearch && { keyword: searchValue }),
}) })
const {
data: suitableSearchResults,
refetch: refetchBySearch,
isLoading: isSearchLoading,
} = useQuery<Suitable[]>({
queryKey: ['suitables', 'search', selectedCategory, isSearch],
queryFn: async () => {
if (!isSearch && !selectedCategory) {
return isInitialLoading ? await getSuitables() : suitableList ?? [] // 검색 상태가 아니면 초기 데이터 반환 임시처리
} else {
return (
suitableList?.filter((item: Suitable) => {
const categoryMatch = !selectedCategory || item.roofMtCd === selectedCategory
const searchMatch = !searchValue || item.productName.includes(searchValue)
return categoryMatch && searchMatch
}) ?? []
)
}
}, },
getNextPageParam: (lastPage: Suitable[], allPages: Suitable[][]) => {
return lastPage.length === itemPerPage ? allPages.length + 1 : undefined
},
initialPageParam: 1,
staleTime: 1000 * 60 * 10, staleTime: 1000 * 60 * 10,
gcTime: 1000 * 60 * 10, gcTime: 1000 * 60 * 10,
enabled: true,
}) })
return { return {
@ -97,10 +99,9 @@ export function useSuitable() {
getSuitableCommCode, getSuitableCommCode,
toCodeName, toCodeName,
toSuitableDetail, toSuitableDetail,
suitables, suitableList,
fetchNextPage, suitableSearchResults,
hasNextPage, refetchBySearch,
isFetchingNextPage, isSearchLoading,
isLoading,
} }
} }

View File

@ -1,10 +1,11 @@
import type { SurveyBasicInfo, SurveyDetailInfo, SurveyDetailRequest, SurveyDetailCoverRequest, SurveyRegistRequest } from '@/types/Survey'
import { useMemo } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import type { SurveyBasicInfo, SurveyDetailInfo, SurveyDetailRequest, SurveyDetailCoverRequest, SurveyRegistRequest } from '@/types/Survey'
import { axiosInstance } from '@/libs/axios'
import { useSurveyFilterStore } from '@/store/surveyFilterStore' import { useSurveyFilterStore } from '@/store/surveyFilterStore'
import { useSessionStore } from '@/store/session'
import { useAxios } from './useAxios'
import { queryStringFormatter } from '@/utils/common-utils' import { queryStringFormatter } from '@/utils/common-utils'
import { useSessionStore } from '@/store/session'
import { useMemo } from 'react'
import { AxiosResponse } from 'axios'
export const requiredFields = [ export const requiredFields = [
{ {
@ -64,9 +65,9 @@ export function useServey(id?: number): {
isDeletingSurvey: boolean isDeletingSurvey: boolean
createSurvey: (survey: SurveyRegistRequest) => Promise<number> createSurvey: (survey: SurveyRegistRequest) => Promise<number>
createSurveyDetail: (params: { surveyId: number; surveyDetail: SurveyDetailCoverRequest }) => void createSurveyDetail: (params: { surveyId: number; surveyDetail: SurveyDetailCoverRequest }) => void
updateSurvey: ({ survey, isTemporary, storeId }: { survey: SurveyRegistRequest; isTemporary: boolean; storeId?: string }) => void updateSurvey: (survey: SurveyRegistRequest) => void
deleteSurvey: () => Promise<boolean> deleteSurvey: () => Promise<boolean>
submitSurvey: (params: { saveId?: number; targetId?: string; storeId?: string; srlNo?: string }) => void submitSurvey: (saveId?: number) => void
validateSurveyDetail: (surveyDetail: SurveyDetailRequest) => string validateSurveyDetail: (surveyDetail: SurveyDetailRequest) => string
getZipCode: (zipCode: string) => Promise<ZipCode[] | null> getZipCode: (zipCode: string) => Promise<ZipCode[] | null>
refetchSurveyList: () => void refetchSurveyList: () => void
@ -74,7 +75,6 @@ export function useServey(id?: number): {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { keyword, searchOption, isMySurvey, sort, offset } = useSurveyFilterStore() const { keyword, searchOption, isMySurvey, sort, offset } = useSurveyFilterStore()
const { session } = useSessionStore() const { session } = useSessionStore()
const { axiosInstance } = useAxios()
const { const {
data: surveyListData, data: surveyListData,
@ -119,7 +119,7 @@ export function useServey(id?: number): {
const { mutateAsync: createSurvey, isPending: isCreatingSurvey } = useMutation({ const { mutateAsync: createSurvey, isPending: isCreatingSurvey } = useMutation({
mutationFn: async (survey: SurveyRegistRequest) => { mutationFn: async (survey: SurveyRegistRequest) => {
const resp = await axiosInstance(null).post<SurveyBasicInfo>('/api/survey-sales', { survey: survey, storeId: session?.storeId ?? null }) const resp = await axiosInstance(null).post<SurveyBasicInfo>('/api/survey-sales', survey)
return resp.data.id ?? 0 return resp.data.id ?? 0
}, },
onSuccess: (data) => { onSuccess: (data) => {
@ -130,14 +130,10 @@ export function useServey(id?: number): {
}) })
const { mutate: updateSurvey, isPending: isUpdatingSurvey } = useMutation({ const { mutate: updateSurvey, isPending: isUpdatingSurvey } = useMutation({
mutationFn: async ({ survey, isTemporary, storeId }: { survey: SurveyRegistRequest; isTemporary: boolean; storeId?: string }) => { mutationFn: async (survey: SurveyRegistRequest) => {
console.log('updateSurvey, survey:: ', survey) console.log('updateSurvey, survey:: ', survey)
if (id === undefined) throw new Error('id is required') if (id === undefined) throw new Error('id is required')
const resp = await axiosInstance(null).put<SurveyRegistRequest>(`/api/survey-sales/${id}`, { const resp = await axiosInstance(null).put<SurveyRegistRequest>(`/api/survey-sales/${id}`, survey)
survey: survey,
isTemporary: isTemporary,
storeId: storeId,
})
return resp.data return resp.data
}, },
onSuccess: () => { onSuccess: () => {
@ -170,13 +166,11 @@ export function useServey(id?: number): {
}) })
const { mutateAsync: submitSurvey } = useMutation({ const { mutateAsync: submitSurvey } = useMutation({
mutationFn: async ({ saveId, targetId, storeId, srlNo }: { saveId?: number; targetId?: string; storeId?: string; srlNo?: string }) => { mutationFn: async (saveId?: number) => {
const submitId = saveId ?? id const submitId = saveId ?? id
if (!submitId) throw new Error('id is required') if (!submitId) throw new Error('id is required')
const resp = await axiosInstance(null).patch<boolean>(`/api/survey-sales/${submitId}`, { const resp = await axiosInstance(null).patch<boolean>(`/api/survey-sales/${submitId}`, {
targetId, submit: true,
storeId,
srlNo,
}) })
return resp.data return resp.data
}, },

View File

@ -1,4 +1,3 @@
import { useSpinnerStore } from '@/store/spinnerStore'
import axios from 'axios' import axios from 'axios'
export const axiosInstance = (url: string | null | undefined) => { export const axiosInstance = (url: string | null | undefined) => {

View File

@ -1,8 +1,7 @@
import { useAxios } from '@/hooks/useAxios' import { axiosInstance } from './axios'
export const tracking = async (params: { url: string; data: string }) => { export const tracking = async (params: { url: string; data: string }) => {
const { url, data } = params const { url, data } = params
const { axiosInstance } = useAxios()
const result = await axiosInstance(null).post('/api/tracking', { const result = await axiosInstance(null).post('/api/tracking', {
url, url,
data, data,

View File

@ -10,9 +10,9 @@ export async function middleware(request: NextRequest) {
const session = await getIronSession<SessionData>(cookieStore, sessionOptions) const session = await getIronSession<SessionData>(cookieStore, sessionOptions)
// todo: 로그인 기능 추가 시 주석 해제 // todo: 로그인 기능 추가 시 주석 해제
// if (!session.isLoggedIn) { if (!session.isLoggedIn) {
// return NextResponse.redirect(new URL('/login', request.url)) return NextResponse.redirect(new URL('/login', request.url))
// } }
return NextResponse.next() return NextResponse.next()
} }

View File

@ -8,8 +8,6 @@ import { usePopupController } from '@/store/popupController'
import { useSideNavState } from '@/store/sideNavState' import { useSideNavState } from '@/store/sideNavState'
import { useSessionStore } from '@/store/session' import { useSessionStore } from '@/store/session'
import { tracking } from '@/libs/tracking' import { tracking } from '@/libs/tracking'
import Spinner from '@/components/ui/common/Spinner'
import { useSpinnerStore } from '@/store/spinnerStore'
declare global { declare global {
interface Window { interface Window {
@ -30,7 +28,6 @@ export default function EdgeProvider({ children, sessionData }: EdgeProviderProp
const { reset } = useSideNavState() const { reset } = useSideNavState()
const { setAlertMsg, setAlertBtn, setAlert, setAlert2, setAlert2BtnYes, setAlert2BtnNo } = usePopupController() const { setAlertMsg, setAlertBtn, setAlert, setAlert2, setAlert2BtnYes, setAlert2BtnNo } = usePopupController()
const { session, setSession } = useSessionStore() const { session, setSession } = useSessionStore()
const { isShow, setIsShow } = useSpinnerStore()
/** /**
* *
@ -113,10 +110,5 @@ export default function EdgeProvider({ children, sessionData }: EdgeProviderProp
handlePageEvent(pathname) handlePageEvent(pathname)
}, [pathname]) }, [pathname])
return ( return <>{children}</>
<>
{children}
{isShow && <Spinner />}
</>
)
} }

View File

@ -1,21 +0,0 @@
import { create } from 'zustand'
type SpinnerState = {
isShow: boolean
setIsShow: (isShow: boolean) => void
resetCount: () => void
}
type InitialState = {
isShow: boolean
}
const initialState: InitialState = {
isShow: false,
}
export const useSpinnerStore = create<SpinnerState>((set) => ({
...initialState,
setIsShow: (isShow: boolean) => set({ isShow }),
resetCount: () => set(initialState),
}))

View File

@ -2,9 +2,6 @@ import { create } from 'zustand'
import type { CommCode } from '@/types/CommCode' import type { CommCode } from '@/types/CommCode'
interface SuitableState { interface SuitableState {
/* 초기 데이터 로드 개수*/
itemPerPage: number
/* 공통코드 */ /* 공통코드 */
suitableCommCode: Map<string, CommCode[]> suitableCommCode: Map<string, CommCode[]>
/* 공통코드 설정 */ /* 공통코드 설정 */
@ -26,22 +23,21 @@ interface SuitableState {
setSearchValue: (value: string) => void setSearchValue: (value: string) => void
/* 선택된 아이템 리스트 */ /* 선택된 아이템 리스트 */
selectedItems: Map<number, Set<number>> selectedItems: number[]
/* 선택된 아이템 추가 */ /* 선택된 아이템 추가 */
addSelectedItem: (mainId: number, detailId?: number) => void addSelectedItem: (itemId: number) => void
/* 선택된 아이템 제거 */ /* 선택된 아이템 제거 */
removeSelectedItem: (mainId: number, detailId?: number) => void removeSelectedItem: (itemId: number) => void
/* 선택된 아이템 모두 제거 */ /* 선택된 아이템 모두 제거 */
clearSelectedItems: () => void clearSelectedItems: () => void
} }
export const useSuitableStore = create<SuitableState>((set) => ({ export const useSuitableStore = create<SuitableState>((set) => ({
itemPerPage: 100 as number,
suitableCommCode: new Map() as Map<string, CommCode[]>, suitableCommCode: new Map() as Map<string, CommCode[]>,
isSearch: false as boolean, isSearch: false as boolean,
selectedCategory: '' as string, selectedCategory: '' as string,
searchValue: '' as string, searchValue: '' as string,
selectedItems: new Map() as Map<number, Set<number>>, selectedItems: [] as number[],
/* 공통코드 설정 */ /* 공통코드 설정 */
setSuitableCommCode: (headCode: string, commCode: CommCode[]) => setSuitableCommCode: (headCode: string, commCode: CommCode[]) =>
@ -59,46 +55,17 @@ export const useSuitableStore = create<SuitableState>((set) => ({
setSearchValue: (value: string) => set({ searchValue: value }), setSearchValue: (value: string) => set({ searchValue: value }),
/* 선택된 아이템 추가 */ /* 선택된 아이템 추가 */
addSelectedItem: (mainId: number, detailId?: number) => { addSelectedItem: (itemId: number) =>
if (detailId) { set((state) => ({
// 디테일(하위) 아이템 추가 selectedItems: state.selectedItems.some((i) => i === itemId) ? state.selectedItems : [...state.selectedItems, itemId],
set((state) => { })),
const detailSet = state.selectedItems.get(mainId) || new Set()
detailSet.add(detailId)
state.selectedItems.set(mainId, detailSet)
return { selectedItems: state.selectedItems }
})
} else {
// 메인(상위) 아이템 추가
set((state) => {
state.selectedItems.set(mainId, new Set())
return { selectedItems: state.selectedItems }
})
}
},
/* 선택된 아이템 제거 */ /* 선택된 아이템 제거 */
removeSelectedItem: (mainId: number, detailId?: number) => { removeSelectedItem: (itemId: number) =>
set((state) => { set((state) => ({
const newSelectedItems = new Map(state.selectedItems) selectedItems: state.selectedItems.filter((i) => i !== itemId),
})),
if (!detailId) {
// 메인(상위) 아이템 제거
newSelectedItems.delete(mainId)
return { selectedItems: newSelectedItems }
}
// 디테일(하위) 아이템 제거
const detailSet = state.selectedItems.get(mainId) || new Set()
detailSet.delete(detailId)
// 디테일(하위)하위 아이템이 모두 제거되면 메인 아이템도 제거
detailSet.size === 0 ? newSelectedItems.delete(mainId) : newSelectedItems.set(mainId, detailSet)
return { selectedItems: newSelectedItems }
})
},
/* 선택된 아이템 모두 제거 */ /* 선택된 아이템 모두 제거 */
clearSelectedItems: () => set({ selectedItems: new Map() as Map<number, Set<number>> }), clearSelectedItems: () => set({ selectedItems: [] }),
})) }))

View File

@ -49,6 +49,14 @@
background-size: cover; background-size: cover;
margin-left: 12px; margin-left: 12px;
} }
.btn-arr-up{
display: block;
width: 10px;
height: 6px;
background: url(/assets/images/common/btn_arr_up.svg)no-repeat center;
background-size: cover;
margin-left: 12px;
}
.btn-edit{ .btn-edit{
display: block; display: block;
width: 10px; width: 10px;

View File

@ -98,23 +98,6 @@
color: #8595A7; color: #8595A7;
} }
} }
&.space{
label{
&::after{
top: 8px;
left: 0px;
width: 10px;
height: 2px;
border: none;
background-color: transparent;
transform: translate(50%, 50%);
-ms-transform: none;
}
}
input[type="checkbox"]:checked + label::after{
background-color: #fff;
}
}
} }
// radio box // radio box
@ -218,7 +201,7 @@
} }
} }
input:checked + .slider { input:checked + .slider {
background-color: #0081b5; background-color: #A8B6C7;
&:after { &:after {
content: ''; content: '';
left: 10px; left: 10px;

View File

@ -2,5 +2,3 @@
@forward 'login'; @forward 'login';
@forward 'pop-contents'; @forward 'pop-contents';
@forward 'sub'; @forward 'sub';
@forward 'pdfview';
@forward 'spinner';

View File

@ -1,57 +0,0 @@
@use "../abstracts" as *;
.pdf-contents{
padding: 0 20px;
border-top: 1px solid #ececec;
}
.pdf-cont-head{
align-items: center;
padding: 24px 0 15px;
border-bottom: 2px solid $black-1010;
.pdf-cont-head-tit{
@include defaultFont($font-s-16, $font-w-600, $black-1010);
margin-bottom: 10px;
}
}
.pdf-cont-head-data-wrap{
@include flex(20px);
align-items: center;
.pdf-cont-head-data-tit{
@include defaultFont($font-s-13, $font-w-500, $black-1010);
}
.pdf-cont-head-data{
@include defaultFont($font-s-13, $font-w-400, #FF5656);
}
}
.pdf-cont-body{
padding: 24px 0 0;
}
.pdf-data-tit{
@include defaultFont($font-s-13, $font-w-500, $black-1010);
margin-bottom: 5px;
}
.pdf-table{
margin-bottom: 24px;
table{
width: 100%;
table-layout: fixed;
border-collapse: collapse;
th{
padding: 9.5px;
@include defaultFont($font-s-11, $font-w-500, $black-1010);
border: 1px solid #2E3A59;
background-color: #F5F6FA;
}
td{
padding: 9.5px;
@include defaultFont($font-s-11, $font-w-400, #FF5656);
border: 1px solid #2E3A59;
}
}
}
.pdf-textarea-data{
padding: 10px;
@include defaultFont($font-s-11, $font-w-400, #FF5656);
border: 1px solid $black-1010;
min-height: 150px;
}

View File

@ -103,12 +103,16 @@
@include defaultFont($font-s-13, $font-w-400, $font-c); @include defaultFont($font-s-13, $font-w-400, $font-c);
} }
.pop-data-table-footer{ .pop-data-table-footer{
@include flex(0px);
.pop-data-table-footer-unit{ .pop-data-table-footer-unit{
flex: 1;
padding: 10px; padding: 10px;
@include defaultFont($font-s-13, $font-w-500, $font-c); @include defaultFont($font-s-13, $font-w-500, $font-c);
border-bottom: 1px solid #2E3A59; border-right: 1px solid #2E3A59;
} }
.pop-data-table-footer-data{ .pop-data-table-footer-data{
flex: none;
width: 104px;
padding: 10px; padding: 10px;
@include defaultFont($font-s-13, $font-w-400, $font-c); @include defaultFont($font-s-13, $font-w-400, $font-c);
} }

View File

@ -1,37 +0,0 @@
.spinner-wrap{
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba($color: #101010, $alpha: 0.5);
z-index: 2000000;
}
.loader {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #fff;
box-shadow: 32px 0 #fff, -32px 0 #fff;
position: relative;
animation: flash 0.5s ease-out infinite alternate;
}
@keyframes flash {
0% {
background-color: #FFF2;
box-shadow: 32px 0 #FFF2, -32px 0 #FFF;
}
50% {
background-color: #FFF;
box-shadow: 32px 0 #FFF2, -32px 0 #FFF2;
}
100% {
background-color: #FFF2;
box-shadow: 32px 0 #FFF, -32px 0 #FFF2;
}
}

View File

@ -31,6 +31,5 @@ export type Suitable = {
manuFtCd: string manuFtCd: string
roofMtCd: string roofMtCd: string
roofShCd: string roofShCd: string
detailCnt: number
detail: string detail: string
} }

View File

@ -14,8 +14,6 @@ export type SurveyBasicInfo = {
detailInfo: SurveyDetailInfo | null detailInfo: SurveyDetailInfo | null
regDt: Date regDt: Date
uptDt: Date uptDt: Date
submissionTargetId: string | null
srlNo: string | null //판매점IDyyMMdd000
} }
export type SurveyDetailInfo = { export type SurveyDetailInfo = {
@ -72,8 +70,6 @@ export type SurveyBasicRequest = {
addressDetail: string | null addressDetail: string | null
submissionStatus: boolean submissionStatus: boolean
submissionDate: string | null submissionDate: string | null
submissionTargetId: string | null
srlNo: string | null //판매점IDyyMMdd000
} }
export type SurveyDetailRequest = { export type SurveyDetailRequest = {
@ -131,8 +127,6 @@ export type SurveyRegistRequest = {
submissionStatus: boolean submissionStatus: boolean
submissionDate: string | null submissionDate: string | null
detailInfo: SurveyDetailRequest | null detailInfo: SurveyDetailRequest | null
submissionTargetId: string | null
srlNo: string | null //판매점IDyyMMdd000
} }
export type Mode = 'CREATE' | 'EDIT' | 'READ' | 'TEMP' // 등록 | 수정 | 상세 | 임시저장 export type Mode = 'CREATE' | 'EDIT' | 'READ' | 'TEMP' // 등록 | 수정 | 상세 | 임시저장