Compare commits

...

23 Commits

Author SHA1 Message Date
2e0ff4ae6f feat: 지붕재적합성 조회 페이징&인피니티쿼리 적용, 체크박스 선택 처리 추가 2025-05-22 18:16:31 +09:00
80194efec1 Merge branch 'dev' of https://git.hanasys.jp/qcast3/onsitesurvey into feature/suitable 2025-05-22 17:05:09 +09:00
c76526bf9b chore: Update package.json scripts and enhance middleware session handling
- Added local build and start scripts to package.json for improved local development.
- Commented out login redirection logic in middleware for future implementation.
- Introduced a new checkbox in the sample page for enhanced UI functionality.
- Refactored useAxios to improve request and response handling with better modularization.
- Updated checkbox styles in SCSS for improved visual consistency.
2025-05-22 16:57:54 +09:00
d31a189899 Merge branch 'dev' of https://git.hanasys.jp/qcast3/onsitesurvey into feature/suitable 2025-05-22 16:24:50 +09:00
e8498948df refactor: Update tracking function to utilize useAxios for API calls
- Replaced direct axiosInstance import with useAxios hook for improved API management.
- Enhanced code structure for better readability and maintainability.
2025-05-22 15:07:41 +09:00
0a88c80bd9 refactor: Update Header component to utilize useAxios for API integration
- Replaced direct axiosInstance import with useAxios hook for improved API management.
- Cleaned up imports and enhanced code structure for better readability.
2025-05-22 15:04:59 +09:00
e846b55faa chore: Update environment configuration and enhance API integration
- Added NEXT_PUBLIC_RUN_MODE to .env files for environment differentiation.
- Introduced .env.localhost for local development settings.
- Updated .env.production with the correct API URL.
- Modified package.json scripts to utilize env-cmd for environment-specific commands.
- Created a common configuration module to manage environment-specific settings.
- Refactored useAxios to utilize the new configuration for base URL management.
- Enhanced Footer component to display current configuration values.
2025-05-22 15:00:41 +09:00
ac575d76d1 Merge branch 'dev' of https://git.hanasys.jp/qcast3/onsitesurvey into feature/suitable 2025-05-22 14:15:43 +09:00
2c5ddad29b refactor: Enhance survey hook and integrate spinner functionality
- Refactored useSurvey hook to utilize useAxios for API calls.
- Added spinner visibility management in EdgeProvider to improve loading feedback.
- Cleaned up imports and organized code structure for better readability.
2025-05-22 14:08:23 +09:00
0e7de68f29 feat: Add Spinner component and update styles
- Introduced a new Spinner component for loading indicators.
- Removed unused btn_arr_up.svg asset and related styles.
- Updated radio button styles for improved UI consistency.
- Added new PDF view styles for enhanced document presentation.
- Included spinner styles for better loading visuals.
2025-05-22 13:24:39 +09:00
956e4ed910 Merge pull request 'feature/survey - 조사매물 목록 조회 필터링 조건 변경 및 임시저장 구현' (#42) from feature/survey into dev
Reviewed-on: #42
2025-05-22 10:27:55 +09:00
bdbdf9997f fix: change the SRL_NO at temporary save 2025-05-22 09:05:41 +09:00
a483ffce44 fix: Detailed error resolution
- 단열재 유무 기타 input 창 나오지 않는 오류 해결
- T01 임시저장 데이터 조회 가능하도록 로직 수정정
2025-05-21 18:21:22 +09:00
1057c29995 Merge branch 'dev' of https://git.hanasys.jp/qcast3/onsitesurvey into feature/survey 2025-05-21 17:54:23 +09:00
333943c651 feat: add SUBMISSION_TARGET_ID, SRL_NO Column
- 제출 대상 판매점 ID, 일련번호 컬럼 추가
- 임시 저장 시 일련번호에 '임시저장000' 으로 저장
- 조사매물 목록 조회 필터링 조건 수정
- url 에러 핸들링
2025-05-21 17:53:58 +09:00
e69c45105e Merge branch 'dev' of https://git.hanasys.jp/qcast3/onsitesurvey into feature/survey 2025-05-21 11:17:26 +09:00
1ab4f82c32 Merge branch 'dev' of https://git.hanasys.jp/qcast3/onsitesurvey into feature/survey 2025-05-21 11:09:05 +09:00
2f8823c252 Merge branch 'dev' of https://git.hanasys.jp/qcast3/onsitesurvey into feature/survey 2025-05-21 10:57:12 +09:00
a58c58afca fix: fix prisma syntax for filtering temporary save data for T01 to Read survey list 2025-05-21 10:29:08 +09:00
b3550ff497 Merge branch 'dev' of https://git.hanasys.jp/qcast3/onsitesurvey into feature/survey 2025-05-21 10:19:49 +09:00
67d587acf5 feat: Implement Separate temporary save data logic 2025-05-21 10:19:38 +09:00
7ae297f3b6 Merge branch 'dev' of https://git.hanasys.jp/qcast3/onsitesurvey into feature/survey 2025-05-20 17:56:21 +09:00
905a309a9c fix: survey update, create, submit logic fix
- 조사매물 수정, 작성, 조회 시 각 컴포넌트에 데이터 적용 안되던 문제 해결
- 조사매물 제출 시 데이터 저장 안되는 문제 해결결
2025-05-20 17:56:09 +09:00
47 changed files with 1008 additions and 454 deletions

View File

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

18
.env.localhost Normal file
View File

@ -0,0 +1,18 @@
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,5 +1,6 @@
NEXT_PUBLIC_RUN_MODE=production
#route handler #route handler
NEXT_PUBLIC_API_URL=http://172.30.1.35:3000 NEXT_PUBLIC_API_URL=http://1.248.227.176: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,9 +3,15 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "env-cmd -f .env.localhost 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": {
@ -13,6 +19,7 @@
"@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,6 +20,9 @@ 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
@ -801,6 +804,10 @@ 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'}
@ -808,6 +815,10 @@ 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==}
@ -866,6 +877,11 @@ 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'}
@ -1021,6 +1037,9 @@ 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
@ -1217,6 +1236,10 @@ 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==}
@ -1324,6 +1347,14 @@ 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==}
@ -1423,6 +1454,11 @@ 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'}
@ -2070,11 +2106,19 @@ 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
@ -2123,6 +2167,11 @@ 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: {}
@ -2297,6 +2346,8 @@ 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: {}
@ -2500,6 +2551,8 @@ 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
@ -2622,6 +2675,12 @@ 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
@ -2703,6 +2762,10 @@ 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

@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 207 B

View File

@ -12,7 +12,6 @@ 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')
@ -20,14 +19,13 @@ export async function GET(request: NextRequest) {
SELECT SELECT
msm.id msm.id
, msm.product_name , msm.product_name
, msm.manu_ft_cd , details.detail_cnt
, 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
@ -42,9 +40,7 @@ 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
@ -53,10 +49,6 @@ 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)
@ -66,7 +58,6 @@ 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,34 +19,55 @@ 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 { DETAIL_INFO, ...basicInfo } = body const { detailInfo, ...basicInfo } = body.survey
console.log('body:: ', body) // PUT 요청 시 임시저장 여부 확인 후 임시저장 시 기존 SRL_NO 사용, 기본 저장 시 새로운 SRL_NO 생성
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: {
upsert: { update: convertToSnakeCase(detailInfo),
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)
@ -92,49 +113,24 @@ 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()
if (body.submit) { // 제출 시 기존 SRL_NO 확인 후 '임시저장'으로 시작하면 새로운 SRL_NO 생성
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,6 +1,7 @@
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'
/** /**
* *
*/ */
@ -87,13 +88,14 @@ 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 } }],
}, },
{ {
AND: [{ STORE: { startsWith: params.store } }, { SUBMISSION_STATUS: { equals: true } }], // MUSUBI (시공권한 X) 가 ORDER 에 제출한 매물
AND: [{ SUBMISSION_TARGET_ID: { equals: params.store } }, { SUBMISSION_STATUS: { equals: true } }],
}, },
] ]
break break
@ -101,6 +103,7 @@ 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 } },
{ {
@ -109,8 +112,9 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => {
], ],
}, },
{ {
// MUSUBI (시공권한 O) 가 MUSUBI 에 제출한 매물 + PARTNER 가 제출한 매물
AND: [ AND: [
{ STORE: { equals: params.store } }, { SUBMISSION_TARGET_ID: { 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 } },
@ -119,8 +123,8 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => {
] ]
break break
case 'Builder': // 2차점 시공권한 case 'Builder': // MUSUBI (시공권한 O)
case 'Partner': // Partner case 'Partner': // PARTNER
// 같은 시공ID에서 작성된 매물 // 같은 시공ID에서 작성된 매물
where.AND?.push({ where.AND?.push({
CONSTRUCTION_POINT: { equals: params.builderNo }, CONSTRUCTION_POINT: { equals: params.builderNo },
@ -128,6 +132,21 @@ 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
@ -222,19 +241,44 @@ 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)
const { detailInfo, ...basicInfo } = body // 임시 저장 시 임시저장 + 000 으로 저장
// 기본 저장 시 판매점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,6 +113,10 @@ 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,9 +5,8 @@ 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
@ -24,6 +23,8 @@ 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 SuitableListRaw from './SuitableList' import SuitableList 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 SuitableRaw() { export default function Suitable() {
const [reference, setReference] = useState(true) const [reference, setReference] = useState(true)
const { getSuitableCommCode, refetchBySearch } = useSuitable() const { getSuitableCommCode } = 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,19 +20,13 @@ export default function SuitableRaw() {
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 () => {
@ -62,6 +56,11 @@ export default function SuitableRaw() {
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} />
@ -110,7 +109,7 @@ export default function SuitableRaw() {
</li> </li>
</ul> </ul>
</div> </div>
<SuitableListRaw /> <SuitableList />
</div> </div>
</div> </div>
) )

View File

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

View File

@ -1,11 +1,16 @@
'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"> <button className="btn-frame n-blue icon" onClick={() => router.push('/inquiry/regist')}>
<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.storeNm ?? null, store: session.role === 'Partner' ? null : session.storeNm ?? null,
constructionPoint: session.builderNo ?? null, constructionPoint: session.builderNo ?? null,
}) })
} }

View File

@ -22,8 +22,15 @@ 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('')
// --------------------------------------------------------------
// 권한 // 권한
// 제출권한 ㅇ // 제출권한 ㅇ
@ -34,39 +41,63 @@ 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) => { const handleSave = (isTemporary: boolean, isSubmitProcess = false) => {
const emptyField = validateSurveyDetail(props.data.roof) const emptyField = validateSurveyDetail(props.data.roof)
console.log('handleSave, emptyField:: ', emptyField) const hasEmptyField = emptyField?.trim() !== ''
if (isTemporary) { if (isTemporary) {
tempSaveProcess() hasEmptyField ? tempSaveProcess() : saveProcess(emptyField, false)
} else { } else {
saveProcess(emptyField) saveProcess(emptyField, isSubmitProcess)
} }
} }
const tempSaveProcess = async () => { const tempSaveProcess = async () => {
if (idParam) { if (idParam) {
await updateSurvey(saveData) await updateSurvey({ survey: saveData, isTemporary: true })
router.push(`/survey-sale/detail?id=${idParam}&isTemporary=true`) router.push(`/survey-sale/${idParam}`)
} else { } else {
const id = await createSurvey(saveData) const updatedData = {
router.push(`/survey-sale/detail?id=${id}&isTemporary=true`) ...saveData,
srlNo: '一時保存',
}
const id = await createSurvey(updatedData)
router.push(`/survey-sale/${id}`)
} }
alert('一時保存されました。') alert('一時保存されました。')
} }
@ -78,30 +109,40 @@ export default function ButtonForm(props: {
} }
} }
const saveProcess = async (emptyField: string) => { const saveProcess = async (emptyField: string | null, isSubmitProcess?: boolean) => {
if (emptyField.trim() === '') { if (emptyField?.trim() === '') {
if (idParam) { if (idParam) {
// 수정 페이지에서 작성 후 제출
if (isSubmitProcess) { if (isSubmitProcess) {
saveData = { const updatedData = {
...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 { } else {
const id = await createSurvey(saveData) await updateSurvey({ survey: saveData, isTemporary: false, storeId: session.storeId ?? '' })
if (isSubmitProcess) { router.push(`/survey-sale/${idParam}`)
submitProcess(id)
return
} }
} else {
if (isSubmitProcess) {
const updatedData = {
...saveData,
submissionStatus: true,
submissionDate: new Date().toISOString(),
submissionTargetId: tempTargetId,
}
const id = await createSurvey(updatedData)
submitProcess(id)
} else {
const id = await createSurvey(saveData)
router.push(`/survey-sale/${id}`) 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 {
@ -123,17 +164,25 @@ 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 () => {
setIsSubmitProcess(true) if (Number(routeId)) {
if (routeId) {
submitProcess() submitProcess()
} else { } else {
handleSave(false) handleSave(false, true)
} }
}) })
} }
const submitProcess = async (saveId?: number) => { const submitProcess = async (saveId?: number) => {
await submitSurvey(saveId) await submitSurvey({ saveId: saveId, targetId: tempTargetId, storeId: session.storeId ?? '', srlNo: '一時保存' })
alert('提出されました。') alert('提出されました。')
router.push('/survey-sale') router.push('/survey-sale')
} }
@ -159,7 +208,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} />} {!isSubmit && isSubmiter && <SubmitButton handleSubmit={handleSubmit} setTempTargetId={setTempTargetId} />}
</div> </div>
</div> </div>
)} )}
@ -170,7 +219,7 @@ export default function ButtonForm(props: {
<ListButton /> <ListButton />
<TempButton setMode={setMode} handleSave={handleSave} /> <TempButton setMode={setMode} handleSave={handleSave} />
<SaveButton handleSave={handleSave} /> <SaveButton handleSave={handleSave} />
<SubmitButton handleSubmit={handleSubmit} /> {session?.role !== 'T01' && <SubmitButton handleSubmit={handleSubmit} setTempTargetId={setTempTargetId} />}{' '}
</div> </div>
</div> </div>
)} )}
@ -210,15 +259,20 @@ function EditButton(props: { setMode: (mode: Mode) => void; id: string; mode: Mo
) )
} }
function SubmitButton(props: { handleSubmit: () => void }) { function SubmitButton(props: { handleSubmit: () => void; setTempTargetId: (targetId: string) => void }) {
const { handleSubmit } = props const { handleSubmit, setTempTargetId } = 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>
</>
) )
} }
@ -256,7 +310,6 @@ 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,31 +1,22 @@
'use client' 'use client'
import { useServey } from '@/hooks/useSurvey' import { useServey } from '@/hooks/useSurvey'
import { useParams, useSearchParams } from 'next/navigation' import { useParams } from 'next/navigation'
import { useEffect, useState } from 'react' import { useEffect } 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
const searchParams = useSearchParams() useEffect(() => {
const isTemp = searchParams.get('isTemporary') if (Number.isNaN(Number(id))) {
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>
@ -42,12 +33,12 @@ export default function DataTable() {
<tbody> <tbody>
<tr> <tr>
<th></th> <th></th>
{isTemporary ? ( {surveyDetail?.srlNo?.startsWith('一時保存') ? (
<td> <td>
<span className="text-red-500"></span> <span className="text-red-500"></span>
</td> </td>
) : ( ) : (
<td>{surveyDetail?.id}</td> <td>{surveyDetail?.srlNo}</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, useSearchParams } from 'next/navigation' import { useParams, useRouter, useSearchParams } from 'next/navigation'
import { useServey } from '@/hooks/useSurvey' import { useServey } from '@/hooks/useSurvey'
const roofInfoForm: SurveyDetailRequest = { const roofInfoForm: SurveyDetailRequest = {
@ -58,34 +58,40 @@ 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 id = idParam ?? routeId const modeset = Number(routeId) ? 'READ' : idParam ? 'EDIT' : 'CREATE'
const id = Number(routeId) ? Number(routeId) : Number(idParam)
const { surveyDetail } = useServey(Number(id)) const { surveyDetail, validateSurveyDetail } = useServey(Number(id))
const [mode, setMode] = useState<Mode>(idParam ? 'EDIT' : routeId ? 'READ' : 'CREATE') const [mode, setMode] = useState<Mode>(modeset)
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,6 +230,14 @@ 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() })
} }
@ -237,7 +245,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}` : value, contractCapacity: numericValue ? `${numericValue} ${value}` : '0 ' + value,
}) })
} }
@ -261,7 +269,7 @@ export default function RoofForm(props: {
{mode !== 'READ' && ( {mode !== 'READ' && (
<div className="data-input mb5"> <div className="data-input mb5">
<input <input
type="text" type="number"
id="contractCapacity" id="contractCapacity"
className="input-frame" className="input-frame"
value={roofInfo?.contractCapacity?.split(' ')[0] ?? ''} value={roofInfo?.contractCapacity?.split(' ')[0] ?? ''}
@ -464,17 +472,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 [isEtcSelected, setIsEtcSelected] = useState<boolean>(etcValue !== null && etcValue !== undefined && etcValue !== '') const isSpecialCase = column === 'constructionYear' || column === 'installationAvailability'
const [etcVal, setEtcVal] = useState<string>(etcValue?.toString() ?? '') const showEtcOption = !isSpecialCase
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: typeof detailInfoData = { const updatedData = {
...detailInfoData, ...detailInfoData,
[column]: isEtc ? null : value, [column]: isEtc ? null : value,
[`${column}Etc`]: isEtc ? '' : null, [`${column}Etc`]: isEtc ? '' : null,
@ -485,14 +493,20 @@ 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>) => {
const value = e.target.value setRoofInfo({ ...detailInfoData, [`${column}Etc`]: 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 (
@ -502,7 +516,7 @@ const SelectedBox = ({
name={column} name={column}
id={column} id={column}
disabled={mode === 'READ'} disabled={mode === 'READ'}
value={selectedId ? Number(selectedId) : etcValue !== null ? 'etc' : ''} value={selectedId ? Number(selectedId) : etcValue ? 'etc' : ''}
onChange={handleSelectChange} onChange={handleSelectChange}
> >
{selectBoxOptions[column as keyof typeof selectBoxOptions].map((item) => ( {selectBoxOptions[column as keyof typeof selectBoxOptions].map((item) => (
@ -510,7 +524,7 @@ const SelectedBox = ({
{item.name} {item.name}
</option> </option>
))} ))}
{column !== 'installationAvailability' && column !== 'constructionYear' && ( {showEtcOption && (
<option key="etc" value="etc"> <option key="etc" value="etc">
() ()
</option> </option>
@ -519,23 +533,16 @@ const SelectedBox = ({
</option> </option>
</select> </select>
<div className="data-input"> <div className={`data-input ${column === 'constructionYear' ? 'flex' : ''}`}>
<input <input
type="text" type={column === 'constructionYear' ? 'number' : 'text'}
className="input-frame" className="input-frame"
placeholder="-" placeholder="-"
value={etcVal} value={detailInfoData[`${column}Etc` as keyof SurveyDetailInfo]?.toString() ?? ''}
onChange={handleEtcInputChange} onChange={handleEtcInputChange}
disabled={ disabled={isInputDisabled()}
mode === 'READ'
? true
: column === 'installationAvailability'
? false
: column === 'constructionYear'
? detailInfoData.constructionYear === '1' || detailInfoData.constructionYear === null
: !isEtcSelected
}
/> />
{column === 'constructionYear' && <span></span>}
</div> </div>
</> </>
) )
@ -552,49 +559,51 @@ const RadioSelected = ({
detailInfoData: SurveyDetailInfo detailInfoData: SurveyDetailInfo
setRoofInfo: (roofInfo: SurveyDetailRequest) => void setRoofInfo: (roofInfo: SurveyDetailRequest) => void
}) => { }) => {
let selectedId = detailInfoData?.[column as keyof SurveyDetailInfo] const etcValue = detailInfoData?.[`${column}Etc` as keyof SurveyDetailInfo]
if (column === 'leakTrace') { const [etcChecked, setEtcChecked] = useState<boolean>(Boolean(etcValue))
selectedId = Number(selectedId)
if (!selectedId) selectedId = 2
}
let etcValue = null const selectedId =
if (column !== 'rafterDirection') { column === 'leakTrace' ? Number(detailInfoData?.[column as keyof SurveyDetailInfo]) || 2 : detailInfoData?.[column as keyof SurveyDetailInfo]
etcValue = detailInfoData?.[`${column}Etc` as keyof SurveyDetailInfo]
} const isSpecialColumn = column === 'rafterDirection' || column === 'leakTrace' || column === 'insulationPresence'
const [etcChecked, setEtcChecked] = useState<boolean>(etcValue !== null && etcValue !== undefined && etcValue !== '') const showEtcOption = !isSpecialColumn
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') {
handleBooleanRadioChange(value) setRoofInfo({ ...detailInfoData, leakTrace: value === '1' })
return
} }
if (value === 'etc') { if (value === 'etc') {
setEtcChecked(true) setEtcChecked(true)
setRoofInfo({ ...detailInfoData, [column]: null, [`${column}Etc`]: '' }) setRoofInfo({ ...detailInfoData, [column]: null, [`${column}Etc`]: '' })
} else { return
if (column === 'insulationPresence' && value === '2') {
setEtcChecked(true)
} else {
setEtcChecked(false)
}
setRoofInfo({ ...detailInfoData, [column]: value, [`${column}Etc`]: null })
}
} }
const handleBooleanRadioChange = (value: string) => { const isInsulationPresence = column === 'insulationPresence'
if (value === '1') { const isRafterDirection = column === 'rafterDirection'
setRoofInfo({ ...detailInfoData, leakTrace: true })
} else { setEtcChecked(isInsulationPresence && value === '2')
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>) => {
const value = e.target.value setRoofInfo({ ...detailInfoData, [`${column}Etc`]: 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 (
@ -613,7 +622,7 @@ const RadioSelected = ({
<label htmlFor={`${column}_${item.id}`}>{item.label}</label> <label htmlFor={`${column}_${item.id}`}>{item.label}</label>
</div> </div>
))} ))}
{column !== 'rafterDirection' && column !== 'leakTrace' && column !== 'insulationPresence' && ( {showEtcOption && (
<div className="radio-form-box mb10"> <div className="radio-form-box mb10">
<input <input
type="radio" type="radio"
@ -621,21 +630,21 @@ const RadioSelected = ({
id={`${column}Etc`} id={`${column}Etc`}
value="etc" value="etc"
disabled={mode === 'READ'} disabled={mode === 'READ'}
checked={etcChecked} checked={etcChecked || Boolean(etcValue)}
onChange={handleRadioChange} onChange={handleRadioChange}
/> />
<label htmlFor={`${column}Etc`}> ()</label> <label htmlFor={`${column}Etc`}> ()</label>
</div> </div>
)} )}
{column !== 'leakTrace' && column !== 'rafterDirection' && ( {(showEtcOption || column === 'insulationPresence') && (
<div className="data-input"> <div className="data-input">
<input <input
type="text" type="text"
className="input-frame" className="input-frame"
placeholder="-" placeholder="-"
value={etcVal} value={detailInfoData[`${column}Etc` as keyof SurveyDetailInfo]?.toString() ?? ''}
onChange={handleEtcInputChange} onChange={handleEtcInputChange}
disabled={mode === 'READ' || !etcChecked} disabled={isInputDisabled()}
/> />
</div> </div>
)} )}
@ -655,51 +664,56 @@ 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 [isOtherCheck, setIsOtherCheck] = useState<boolean>(false) const isRoofMaterial = column === 'roofMaterial'
const [otherValue, setOtherValue] = useState<string>(roofInfo?.[`${column}Etc` as keyof SurveyDetailInfo]?.toString() ?? '') const selectedValues = makeNumArr(String(roofInfo[column as keyof SurveyDetailInfo] ?? ''))
const handleCheckbox = (id: number) => { const handleCheckbox = (id: number) => {
const value = makeNumArr(String(roofInfo[column as keyof SurveyDetailInfo] ?? '')) const isOtherSelected = Boolean(etcValue)
const isOtherSelected = roofInfo?.[`${column}Etc` as keyof SurveyDetailInfo] !== null
let newValue: string[] let newValue: string[]
if (value.includes(String(id))) {
newValue = value.filter((v) => v !== String(id))
} else {
if (column === 'roofMaterial') {
const totalSelected = value.length + (isOtherSelected ? 1 : 0)
if (selectedValues.includes(String(id))) {
newValue = selectedValues.filter((v) => v !== String(id))
} else {
if (isRoofMaterial) {
const totalSelected = selectedValues.length + (isOtherSelected ? 1 : 0)
if (totalSelected >= 2) { if (totalSelected >= 2) {
alert('屋根材は最大2個まで選択できます。') alert('屋根材は最大2個まで選択できます。')
return return
} }
} }
newValue = [...value, String(id)] newValue = [...selectedValues, String(id)]
} }
setRoofInfo({ ...roofInfo, [column]: newValue.join(',') }) setRoofInfo({ ...roofInfo, [column]: newValue.join(',') })
} }
const handleOtherCheckbox = () => { const handleOtherCheckbox = () => {
if (column === 'roofMaterial') { if (isRoofMaterial) {
const value = makeNumArr(String(roofInfo[column as keyof SurveyDetailInfo] ?? '')) const currentSelected = selectedValues.length
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('')
setRoofInfo({ ...roofInfo, [`${column}Etc`]: newIsOtherCheck ? '' : null }) // 기타 선택 해제 시 값도 null로 설정
setRoofInfo({
...roofInfo,
[`${column}Etc`]: newIsOtherCheck ? '' : null,
})
} }
const handleOtherInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleOtherInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value setRoofInfo({ ...roofInfo, [`${column}Etc`]: e.target.value })
setOtherValue(value) }
setRoofInfo({ ...roofInfo, [`${column}Etc`]: value })
const isInputDisabled = () => {
return mode === 'READ' || (!isOtherCheck && !etcValue)
} }
return ( return (
@ -710,7 +724,7 @@ const MultiCheck = ({
<input <input
type="checkbox" type="checkbox"
id={`${column}_${item.id}`} id={`${column}_${item.id}`}
checked={makeNumArr(String(roofInfo[column as keyof SurveyDetailInfo] ?? '')).includes(String(item.id))} checked={selectedValues.includes(String(item.id))}
disabled={mode === 'READ'} disabled={mode === 'READ'}
onChange={() => handleCheckbox(item.id)} onChange={() => handleCheckbox(item.id)}
/> />
@ -721,7 +735,7 @@ const MultiCheck = ({
<input <input
type="checkbox" type="checkbox"
id={`${column}Etc`} id={`${column}Etc`}
checked={roofInfo?.[`${column}Etc` as keyof SurveyDetailInfo] !== null} checked={isOtherCheck || Boolean(etcValue)}
disabled={mode === 'READ'} disabled={mode === 'READ'}
onChange={handleOtherCheckbox} onChange={handleOtherCheckbox}
/> />
@ -733,9 +747,9 @@ const MultiCheck = ({
type="text" type="text"
className="input-frame" className="input-frame"
placeholder="-" placeholder="-"
value={otherValue} value={roofInfo[`${column}Etc` as keyof SurveyDetailInfo]?.toString() ?? ''}
onChange={handleOtherInputChange} onChange={handleOtherInputChange}
disabled={mode === 'READ' || !isOtherCheck} disabled={isInputDisabled()}
/> />
</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 } from 'react' import { useEffect, useState, useMemo, useRef } from 'react'
import { useRouter } from 'next/navigation' import { useRouter, usePathname } 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,13 +11,20 @@ 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)
const { session } = useSessionStore() useEffect(() => {
setOffset(0)
setHeldSurveyList([])
}, [pathname])
useEffect(() => { useEffect(() => {
if (!session.isLoggedIn || !('data' in surveyList)) return if (!session.isLoggedIn || !('data' in surveyList)) return
@ -32,30 +39,25 @@ export default function ListTable() {
setHeldSurveyList([]) setHeldSurveyList([])
setHasMore(false) setHasMore(false)
} }
}, [surveyList, offset, session]) }, [surveyList, offset, session.isLoggedIn])
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 ?? ''} userId={session?.userId ?? ''} /> <SearchForm memberRole={session?.role ?? ''} userNm={session?.userNm ?? ''} />
{heldSurveyList.length > 0 ? (
<div className="sale-frame"> <div className="sale-frame">
{heldSurveyList.length > 0 ? (
<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.id}</div> <div className="sale-item-num">{survey.srlNo}</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>
@ -68,15 +70,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, userId }: { memberRole: string; userId: string }) { export default function SearchForm({ memberRole, userNm }: { memberRole: string; userNm: 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, userId }: { memberRole: string;
<input <input
type="checkbox" type="checkbox"
id="ch01" id="ch01"
checked={isMySurvey === userId} checked={isMySurvey === userNm}
onChange={() => { onChange={() => {
setIsMySurvey(isMySurvey === userId ? null : userId) setIsMySurvey(isMySurvey === userNm ? null : userNm)
}} }}
/> />
<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/basic-info')}></button> <button className="main-bx-arr" onClick={() => router.push('/survey-sale/regist')}></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,6 +1,7 @@
'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 (
@ -11,6 +12,9 @@ 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,14 +13,13 @@ 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

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

View File

@ -0,0 +1,20 @@
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

@ -0,0 +1,13 @@
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

@ -0,0 +1,19 @@
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

@ -0,0 +1,13 @@
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

@ -0,0 +1,13 @@
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

119
src/hooks/useAxios.ts Normal file
View File

@ -0,0 +1,119 @@
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,24 +1,37 @@
import { useQuery } from '@tanstack/react-query' import { useInfiniteQuery } from '@tanstack/react-query'
import { axiosInstance, transformObjectKeys } from '@/libs/axios' import { 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 { selectedCategory, searchValue, suitableCommCode, setSuitableCommCode, isSearch } = useSuitableStore() const { itemPerPage, 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 response = await axiosInstance(null).get<Suitable[]>('/api/suitable/list', { const params: Record<string, string | number> = {
params: { pageNumber: pageNumber || 1,
pageNumber: 1, itemPerPage: itemPerPage,
itemPerPage: 1000, }
ids: '', if (ids) params.ids = ids
category: '', if (category) params.category = category
keyword: '', if (keyword) params.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)
@ -26,16 +39,6 @@ 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) {
@ -63,35 +66,30 @@ export function useSuitable() {
} }
} }
const { data: suitableList, isLoading: isInitialLoading } = useQuery<Suitable[]>({
queryKey: ['suitables', 'list'],
queryFn: async () => await getSuitables(),
staleTime: 1000 * 60 * 10, // 10분
gcTime: 1000 * 60 * 10, // 10분
})
const { const {
data: suitableSearchResults, data: suitables,
refetch: refetchBySearch, fetchNextPage,
isLoading: isSearchLoading, hasNextPage,
} = useQuery<Suitable[]>({ isFetchingNextPage,
queryKey: ['suitables', 'search', selectedCategory, isSearch], isLoading,
queryFn: async () => { isError,
if (!isSearch && !selectedCategory) { error,
return isInitialLoading ? await getSuitables() : suitableList ?? [] // 검색 상태가 아니면 초기 데이터 반환 임시처리 } = useInfiniteQuery<Suitable[]>({
} else { queryKey: ['suitables', 'list', selectedCategory, isSearch],
return ( queryFn: async (context) => {
suitableList?.filter((item: Suitable) => { const pageParam = context.pageParam as number
const categoryMatch = !selectedCategory || item.roofMtCd === selectedCategory return await getSuitables({
const searchMatch = !searchValue || item.productName.includes(searchValue) pageNumber: pageParam,
return categoryMatch && searchMatch ...(selectedCategory && { category: selectedCategory }),
}) ?? [] ...(isSearch && { keyword: searchValue }),
) })
}
}, },
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 {
@ -99,9 +97,10 @@ export function useSuitable() {
getSuitableCommCode, getSuitableCommCode,
toCodeName, toCodeName,
toSuitableDetail, toSuitableDetail,
suitableList, suitables,
suitableSearchResults, fetchNextPage,
refetchBySearch, hasNextPage,
isSearchLoading, isFetchingNextPage,
isLoading,
} }
} }

View File

@ -1,11 +1,10 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import type { SurveyBasicInfo, SurveyDetailInfo, SurveyDetailRequest, SurveyDetailCoverRequest, SurveyRegistRequest } from '@/types/Survey' import type { SurveyBasicInfo, SurveyDetailInfo, SurveyDetailRequest, SurveyDetailCoverRequest, SurveyRegistRequest } from '@/types/Survey'
import { axiosInstance } from '@/libs/axios'
import { useSurveyFilterStore } from '@/store/surveyFilterStore'
import { queryStringFormatter } from '@/utils/common-utils'
import { useSessionStore } from '@/store/session'
import { useMemo } from 'react' import { useMemo } from 'react'
import { AxiosResponse } from 'axios' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useSurveyFilterStore } from '@/store/surveyFilterStore'
import { useSessionStore } from '@/store/session'
import { useAxios } from './useAxios'
import { queryStringFormatter } from '@/utils/common-utils'
export const requiredFields = [ export const requiredFields = [
{ {
@ -65,9 +64,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: SurveyRegistRequest) => void updateSurvey: ({ survey, isTemporary, storeId }: { survey: SurveyRegistRequest; isTemporary: boolean; storeId?: string }) => void
deleteSurvey: () => Promise<boolean> deleteSurvey: () => Promise<boolean>
submitSurvey: (saveId?: number) => void submitSurvey: (params: { saveId?: number; targetId?: string; storeId?: string; srlNo?: string }) => 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
@ -75,6 +74,7 @@ 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) const resp = await axiosInstance(null).post<SurveyBasicInfo>('/api/survey-sales', { survey: survey, storeId: session?.storeId ?? null })
return resp.data.id ?? 0 return resp.data.id ?? 0
}, },
onSuccess: (data) => { onSuccess: (data) => {
@ -130,10 +130,14 @@ export function useServey(id?: number): {
}) })
const { mutate: updateSurvey, isPending: isUpdatingSurvey } = useMutation({ const { mutate: updateSurvey, isPending: isUpdatingSurvey } = useMutation({
mutationFn: async (survey: SurveyRegistRequest) => { mutationFn: async ({ survey, isTemporary, storeId }: { survey: SurveyRegistRequest; isTemporary: boolean; storeId?: string }) => {
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}`, survey) const resp = await axiosInstance(null).put<SurveyRegistRequest>(`/api/survey-sales/${id}`, {
survey: survey,
isTemporary: isTemporary,
storeId: storeId,
})
return resp.data return resp.data
}, },
onSuccess: () => { onSuccess: () => {
@ -166,11 +170,13 @@ export function useServey(id?: number): {
}) })
const { mutateAsync: submitSurvey } = useMutation({ const { mutateAsync: submitSurvey } = useMutation({
mutationFn: async (saveId?: number) => { mutationFn: async ({ saveId, targetId, storeId, srlNo }: { saveId?: number; targetId?: string; storeId?: string; srlNo?: string }) => {
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}`, {
submit: true, targetId,
storeId,
srlNo,
}) })
return resp.data return resp.data
}, },

View File

@ -1,3 +1,4 @@
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,7 +1,8 @@
import { axiosInstance } from './axios' import { useAxios } from '@/hooks/useAxios'
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,6 +8,8 @@ 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 {
@ -28,6 +30,7 @@ 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()
/** /**
* *
@ -110,5 +113,10 @@ export default function EdgeProvider({ children, sessionData }: EdgeProviderProp
handlePageEvent(pathname) handlePageEvent(pathname)
}, [pathname]) }, [pathname])
return <>{children}</> return (
<>
{children}
{isShow && <Spinner />}
</>
)
} }

21
src/store/spinnerStore.ts Normal file
View File

@ -0,0 +1,21 @@
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,6 +2,9 @@ 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[]>
/* 공통코드 설정 */ /* 공통코드 설정 */
@ -23,21 +26,22 @@ interface SuitableState {
setSearchValue: (value: string) => void setSearchValue: (value: string) => void
/* 선택된 아이템 리스트 */ /* 선택된 아이템 리스트 */
selectedItems: number[] selectedItems: Map<number, Set<number>>
/* 선택된 아이템 추가 */ /* 선택된 아이템 추가 */
addSelectedItem: (itemId: number) => void addSelectedItem: (mainId: number, detailId?: number) => void
/* 선택된 아이템 제거 */ /* 선택된 아이템 제거 */
removeSelectedItem: (itemId: number) => void removeSelectedItem: (mainId: number, detailId?: 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: [] as number[], selectedItems: new Map() as Map<number, Set<number>>,
/* 공통코드 설정 */ /* 공통코드 설정 */
setSuitableCommCode: (headCode: string, commCode: CommCode[]) => setSuitableCommCode: (headCode: string, commCode: CommCode[]) =>
@ -55,17 +59,46 @@ export const useSuitableStore = create<SuitableState>((set) => ({
setSearchValue: (value: string) => set({ searchValue: value }), setSearchValue: (value: string) => set({ searchValue: value }),
/* 선택된 아이템 추가 */ /* 선택된 아이템 추가 */
addSelectedItem: (itemId: number) => addSelectedItem: (mainId: number, detailId?: number) => {
set((state) => ({ if (detailId) {
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: (itemId: number) => removeSelectedItem: (mainId: number, detailId?: number) => {
set((state) => ({ set((state) => {
selectedItems: state.selectedItems.filter((i) => i !== itemId), const newSelectedItems = new Map(state.selectedItems)
})),
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: [] }), clearSelectedItems: () => set({ selectedItems: new Map() as Map<number, Set<number>> }),
})) }))

View File

@ -49,14 +49,6 @@
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,6 +98,23 @@
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
@ -201,7 +218,7 @@
} }
} }
input:checked + .slider { input:checked + .slider {
background-color: #A8B6C7; background-color: #0081b5;
&:after { &:after {
content: ''; content: '';
left: 10px; left: 10px;

View File

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

View File

@ -0,0 +1,57 @@
@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,16 +103,12 @@
@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-right: 1px solid #2E3A59; border-bottom: 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

@ -0,0 +1,37 @@
.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,5 +31,6 @@ export type Suitable = {
manuFtCd: string manuFtCd: string
roofMtCd: string roofMtCd: string
roofShCd: string roofShCd: string
detailCnt: number
detail: string detail: string
} }

View File

@ -14,6 +14,8 @@ 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 = {
@ -70,6 +72,8 @@ 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 = {
@ -127,6 +131,8 @@ 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' // 등록 | 수정 | 상세 | 임시저장