Compare commits

..

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

47 changed files with 453 additions and 1007 deletions

View File

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

View File

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

View File

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

View File

@ -3,15 +3,9 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "env-cmd -f .env.localhost next dev --turbopack",
"dev": "next dev --turbopack",
"build": "next build",
"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"
},
"dependencies": {
@ -19,7 +13,6 @@
"@tanstack/react-query": "^5.71.0",
"@tanstack/react-query-devtools": "^5.71.0",
"axios": "^1.8.4",
"env-cmd": "^10.1.0",
"iron-session": "^8.0.4",
"lucide": "^0.503.0",
"mssql": "^11.0.1",

63
pnpm-lock.yaml generated
View File

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

View File

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

After

Width:  |  Height:  |  Size: 207 B

View File

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

View File

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

View File

@ -1,7 +1,6 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/libs/prisma'
import { convertToSnakeCase } from '@/utils/common-utils'
import { equal } from 'assert'
/**
*
*/
@ -88,14 +87,13 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => {
switch (params.role) {
case 'Admin': // 1차점
// 같은 판매점에서 작성된 매물 + 2차점에서 제출받은 매물
where.OR = [
{
// 같은 판매점에서 작성한 제출/제출되지 않은 매물
AND: [{ STORE: { equals: params.store } }],
},
{
// MUSUBI (시공권한 X) 가 ORDER 에 제출한 매물
AND: [{ SUBMISSION_TARGET_ID: { equals: params.store } }, { SUBMISSION_STATUS: { equals: true } }],
AND: [{ STORE: { startsWith: params.store } }, { SUBMISSION_STATUS: { equals: true } }],
},
]
break
@ -103,7 +101,6 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => {
case 'Admin_Sub': // 2차점
where.OR = [
{
// MUSUBI (시공권한 X) 같은 판매점에서 작성한 제출/제출되지 않은 매물
AND: [
{ STORE: { equals: params.store } },
{
@ -112,9 +109,8 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => {
],
},
{
// MUSUBI (시공권한 O) 가 MUSUBI 에 제출한 매물 + PARTNER 가 제출한 매물
AND: [
{ SUBMISSION_TARGET_ID: { equals: params.store } },
{ STORE: { equals: params.store } },
{ CONSTRUCTION_POINT: { not: null } },
{ CONSTRUCTION_POINT: { not: '' } },
{ SUBMISSION_STATUS: { equals: true } },
@ -123,8 +119,8 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => {
]
break
case 'Builder': // MUSUBI (시공권한 O)
case 'Partner': // PARTNER
case 'Builder': // 2차점 시공권한
case 'Partner': // Partner
// 같은 시공ID에서 작성된 매물
where.AND?.push({
CONSTRUCTION_POINT: { equals: params.builderNo },
@ -132,21 +128,6 @@ const createMemberRoleCondition = (params: SearchParams): WhereCondition => {
break
case 'T01':
where.OR = [
{
NOT: {
SRL_NO: {
startsWith: '一時保存',
},
},
},
{
STORE: {
equals: params.store,
},
},
]
break
case 'User':
// 모든 매물 조회 가능 (추가 조건 없음)
break
@ -238,47 +219,22 @@ export async function PUT(request: Request) {
}
}
export async function POST(request: Request) {
export async function POST(request: Request) {
try {
const body = await request.json()
console.log('body:: ', 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')
const { detailInfo, ...basicInfo } = body
// @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({
data: {
...convertToSnakeCase(basicInfo),
SRL_NO: newSrlNo,
DETAIL_INFO: {
create: convertToSnakeCase(detailInfo),
},
},
create: convertToSnakeCase(detailInfo)
}
}
})
return NextResponse.json(result)
} catch (error) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,8 +2,8 @@
import LoadMoreButton from '@/components/LoadMoreButton'
import { useServey } from '@/hooks/useSurvey'
import { useEffect, useState, useMemo, useRef } from 'react'
import { useRouter, usePathname } from 'next/navigation'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import SearchForm from './SearchForm'
import { useSurveyFilterStore } from '@/store/surveyFilterStore'
import { useSessionStore } from '@/store/session'
@ -11,20 +11,13 @@ import type { SurveyBasicInfo } from '@/types/Survey'
export default function ListTable() {
const router = useRouter()
const pathname = usePathname()
const { surveyList, isLoadingSurveyList } = useServey()
const { offset, setOffset } = useSurveyFilterStore()
const { session } = useSessionStore()
const [heldSurveyList, setHeldSurveyList] = useState<SurveyBasicInfo[]>([])
const [hasMore, setHasMore] = useState(false)
useEffect(() => {
setOffset(0)
setHeldSurveyList([])
}, [pathname])
const { session } = useSessionStore()
useEffect(() => {
if (!session.isLoggedIn || !('data' in surveyList)) return
@ -39,25 +32,30 @@ export default function ListTable() {
setHeldSurveyList([])
setHasMore(false)
}
}, [surveyList, offset, session.isLoggedIn])
}, [surveyList, offset, session])
const handleDetailClick = (id: number) => {
router.push(`/survey-sale/${id}`)
}
const handleItemsInit = () => {
setHeldSurveyList([])
setOffset(0)
}
// TODO: 로딩 처리 필요
return (
<>
<SearchForm memberRole={session?.role ?? ''} userNm={session?.userNm ?? ''} />
<SearchForm memberRole={session?.role ?? ''} userId={session?.userId ?? ''} />
{heldSurveyList.length > 0 ? (
<div className="sale-frame">
{heldSurveyList.length > 0 ? (
<ul className="sale-list-wrap">
<ul className="sale-list-wrap">
{heldSurveyList.map((survey) => (
<li className="sale-list-item cursor-pointer" key={survey.id} onClick={() => handleDetailClick(survey.id)}>
<div className="sale-item-bx">
<div className="sale-item-date-bx">
<div className="sale-item-num">{survey.srlNo}</div>
<div className="sale-item-num">{survey.id}</div>
<div className="sale-item-date">{survey.investigationDate}</div>
</div>
<div className="sale-item-tit">{survey.buildingName}</div>
@ -67,18 +65,18 @@ export default function ListTable() {
<div className="sale-item-update">{new Date(survey.uptDt).toLocaleString()}</div>
</div>
</div>
</li>
))}
</ul>
) : (
<div className="compliace-nosearch">
<span className="mb10"></span>
</div>
)}
</li>
))}
</ul>
<div className="sale-edit-btn">
<LoadMoreButton hasMore={hasMore} onLoadMore={() => setOffset(offset + 10)} />
</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 { useState } from 'react'
export default function SearchForm({ memberRole, userNm }: { memberRole: string; userNm: string }) {
export default function SearchForm({ memberRole, userId }: { memberRole: string; userId: string }) {
const router = useRouter()
const { setSearchOption, setSort, setIsMySurvey, setKeyword, isMySurvey, keyword, searchOption, sort } = useSurveyFilterStore()
const [searchKeyword, setSearchKeyword] = useState(keyword)
@ -75,9 +75,9 @@ export default function SearchForm({ memberRole, userNm }: { memberRole: string;
<input
type="checkbox"
id="ch01"
checked={isMySurvey === userNm}
checked={isMySurvey === userId}
onChange={() => {
setIsMySurvey(isMySurvey === userNm ? null : userNm)
setIsMySurvey(isMySurvey === userId ? null : userId)
}}
/>
<label htmlFor="ch01"></label>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -49,6 +49,14 @@
background-size: cover;
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{
display: block;
width: 10px;

View File

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

View File

@ -1,6 +1,4 @@
@forward 'main';
@forward 'login';
@forward 'pop-contents';
@forward 'sub';
@forward 'pdfview';
@forward 'spinner';
@forward 'sub';

View File

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

View File

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

View File

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

View File

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

View File

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