feature/survey - 조사매물 목록 조회 필터링 조건 변경 및 임시저장 구현 #42

Merged
swyoo merged 12 commits from feature/survey into dev 2025-05-22 10:27:55 +09:00
33 changed files with 1395 additions and 251 deletions
Showing only changes of commit 7ae297f3b6 - Show all commits

View File

@ -10,8 +10,8 @@ NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120
NEXT_PUBLIC_INQUIRY_API_URL=http://1.248.227.176:38080
#QPARTNER 로그인 api
DB_HOST=asdf
DB_USER=asdf
DB_PASSWORD=asdf
DB_DATABASE=asdf
DB_HOST=202.218.61.226
DB_USER=readonly
DB_PASSWORD=aAjmFW12iHKW84l1
DB_DATABASE=qpartners
DB_PORT=3306

View File

@ -8,8 +8,8 @@ NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120
NEXT_PUBLIC_INQUIRY_API_URL=http://1.248.227.176:38080
#QPARTNER 로그인 api
DB_HOST=asdf
DB_USER=asdf
DB_PASSWORD=asdf
DB_DATABASE=asdf
DB_HOST=202.218.61.226
DB_USER=readonly
DB_PASSWORD=aAjmFW12iHKW84l1
DB_DATABASE=qpartners
DB_PORT=3306

View File

@ -58,3 +58,20 @@ session에 있는 role 키로 구분한다
session.role === 'Partner'
- 이외의 경우 -> 굳이 체크할 필요 없어보임\
session.role === 'User'
# 지붕재 적합성 TODO
```
const suitableCheck = (value: string) => {
if (value === '×') {
return <i className="compliance-icon x" />
} else if (value === 'ー') {
return <i className="compliance-icon quest" />
} else {
return <i className="compliance-icon check" />
}
}
```
- 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요

View File

@ -16,6 +16,7 @@
"iron-session": "^8.0.4",
"lucide": "^0.503.0",
"mssql": "^11.0.1",
"mysql2": "^3.14.1",
"next": "15.2.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@ -27,6 +28,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/mysql": "^2.15.27",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",

89
pnpm-lock.yaml generated
View File

@ -29,6 +29,9 @@ importers:
mssql:
specifier: ^11.0.1
version: 11.0.1
mysql2:
specifier: ^3.14.1
version: 3.14.1
next:
specifier: 15.2.4
version: 15.2.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.87.0)
@ -57,6 +60,9 @@ importers:
'@tailwindcss/postcss':
specifier: ^4
version: 4.0.17
'@types/mysql':
specifier: ^2.15.27
version: 2.15.27
'@types/node':
specifier: ^20
version: 20.17.28
@ -676,6 +682,9 @@ packages:
'@tediousjs/connection-string@0.5.0':
resolution: {integrity: sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ==}
'@types/mysql@2.15.27':
resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==}
'@types/node@20.17.28':
resolution: {integrity: sha512-DHlH/fNL6Mho38jTy7/JT7sn2wnXI+wULR6PV4gy4VHLVvnrV/d3pHAMQHhc4gjdLmK2ZiPoMxzp6B3yRajLSQ==}
@ -712,6 +721,10 @@ packages:
engines: {node: '>= 4.5.0'}
hasBin: true
aws-ssl-profiles@1.1.2:
resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==}
engines: {node: '>= 6.0.0'}
axios@1.8.4:
resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==}
@ -826,6 +839,10 @@ packages:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'}
detect-libc@1.0.3:
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
engines: {node: '>=0.10'}
@ -911,6 +928,9 @@ packages:
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
generate-function@2.3.1:
resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
@ -994,6 +1014,9 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
is-property@1.0.2:
resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
is-wsl@3.1.0:
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
engines: {node: '>=16'}
@ -1112,6 +1135,17 @@ packages:
lodash.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
long@5.3.2:
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
lru-cache@7.18.3:
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
engines: {node: '>=12'}
lru.min@1.1.2:
resolution: {integrity: sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==}
engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'}
lucide@0.503.0:
resolution: {integrity: sha512-ZAVlxBU4dbSUAVidb2eT0fH3bTtKCj7M2aZNAVsFOrcnazvYJFu6I8OxFE+Fmx5XNf22Cw4Ln3NBHfBxNfoFOw==}
@ -1139,6 +1173,14 @@ packages:
engines: {node: '>=18'}
hasBin: true
mysql2@3.14.1:
resolution: {integrity: sha512-7ytuPQJjQB8TNAYX/H2yhL+iQOnIBjAMam361R7UAL0lOVXWjtdrmoL9HYKqKoLp/8UUTRcvo1QPvK9KL7wA8w==}
engines: {node: '>= 8.0'}
named-placeholders@1.1.3:
resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==}
engines: {node: '>=12.0.0'}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@ -1275,6 +1317,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
seq-queue@0.0.5:
resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==}
sharp@0.33.5:
resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@ -1289,6 +1334,10 @@ packages:
sprintf-js@1.1.3:
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
sqlstring@2.3.3:
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
engines: {node: '>= 0.6'}
stackblur-canvas@2.7.0:
resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==}
engines: {node: '>=0.1.14'}
@ -1890,6 +1939,10 @@ snapshots:
'@tediousjs/connection-string@0.5.0': {}
'@types/mysql@2.15.27':
dependencies:
'@types/node': 20.17.28
'@types/node@20.17.28':
dependencies:
undici-types: 6.19.8
@ -1923,6 +1976,8 @@ snapshots:
atob@2.1.2: {}
aws-ssl-profiles@1.1.2: {}
axios@1.8.4:
dependencies:
follow-redirects: 1.15.9
@ -2041,6 +2096,8 @@ snapshots:
delayed-stream@1.0.0: {}
denque@2.1.0: {}
detect-libc@1.0.3:
optional: true
@ -2141,6 +2198,10 @@ snapshots:
function-bind@1.1.2: {}
generate-function@2.3.1:
dependencies:
is-property: 1.0.2
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
@ -2230,6 +2291,8 @@ snapshots:
is-number@7.0.0:
optional: true
is-property@1.0.2: {}
is-wsl@3.1.0:
dependencies:
is-inside-container: 1.0.0
@ -2346,6 +2409,12 @@ snapshots:
lodash.once@4.1.1: {}
long@5.3.2: {}
lru-cache@7.18.3: {}
lru.min@1.1.2: {}
lucide@0.503.0: {}
math-intrinsics@1.1.0: {}
@ -2375,6 +2444,22 @@ snapshots:
transitivePeerDependencies:
- supports-color
mysql2@3.14.1:
dependencies:
aws-ssl-profiles: 1.1.2
denque: 2.1.0
generate-function: 2.3.1
iconv-lite: 0.6.3
long: 5.3.2
lru.min: 1.1.2
named-placeholders: 1.1.3
seq-queue: 0.0.5
sqlstring: 2.3.3
named-placeholders@1.1.3:
dependencies:
lru-cache: 7.18.3
nanoid@3.3.11: {}
native-duplexpair@1.0.0: {}
@ -2508,6 +2593,8 @@ snapshots:
semver@7.7.1: {}
seq-queue@0.0.5: {}
sharp@0.33.5:
dependencies:
color: 4.2.3
@ -2544,6 +2631,8 @@ snapshots:
sprintf-js@1.1.3: {}
sqlstring@2.3.3: {}
stackblur-canvas@2.7.0:
optional: true

View File

@ -1,5 +1,4 @@
import { NextResponse } from 'next/server'
import { axiosInstance } from '@/libs/axios'
export async function POST(req: Request) {

View File

@ -1,8 +1,8 @@
import { sessionOptions } from '@/libs/session'
import { SessionData } from '@/types/Auth'
import { getIronSession } from 'iron-session'
import type { SessionData } from '@/types/Auth'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
import { getIronSession } from 'iron-session'
import { sessionOptions } from '@/libs/session'
export async function GET(request: Request) {
const cookieStore = await cookies()

View File

@ -1,12 +1,10 @@
import type { SessionData } from '@/types/Auth'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
import { getIronSession } from 'iron-session'
import { axiosInstance } from '@/libs/axios'
import { sessionOptions } from '@/libs/session'
import type { SessionData } from '@/types/Auth'
export async function POST(request: Request) {
const { loginId, pwd } = await request.json()

View File

@ -0,0 +1,44 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/libs/prisma'
import type { CommCode } from '@/types/CommCode'
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const headCode = searchParams.get('headCode')
// @ts-ignore
const headCd = await prisma.BC_COMM_H.findFirst({
where: {
HEAD_ID: headCode,
},
select: {
HEAD_CD: true,
},
})
if (!headCd) {
return NextResponse.json({ error: `${headCode}를 찾을 수 없습니다` }, { status: 404 })
}
// @ts-ignore
const roofMaterials: CommCode[] = await prisma.BC_COMM_L.findMany({
where: {
HEAD_CD: headCd.HEAD_CD,
},
select: {
HEAD_CD: true,
CODE: true,
CODE_JP: true,
},
orderBy: {
CODE: 'asc',
},
})
return NextResponse.json(roofMaterials)
} catch (error) {
console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error)
return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 })
}
}

View File

@ -0,0 +1,129 @@
import type { SessionData } from '@/types/Auth'
import { NextResponse } from 'next/server'
import { cookies } from 'next/headers'
import { getIronSession } from 'iron-session'
import executeQuery from '@/libs/partner'
import { sessionOptions } from '@/libs/session'
export async function POST(request: Request) {
const cookieStore = await cookies()
const session = await getIronSession<SessionData>(cookieStore, sessionOptions)
const { loginId, pwd } = await request.json()
const sql = `
SELECT
r.data_id,
u.id AS user_id,
u.login_id AS user_login_id,
u.password AS user_password,
u.user_name AS user_name,
u.user_name_kana AS user_name_kana,
u.sei AS user_sei,
u.mei AS user_mei,
u.sei_kana AS user_sei_kana,
u.mei_kana AS user_mei_kana,
u.user_tel AS user_tel,
u.user_fax AS user_fax,
u.status AS user_status,
u.seko_id AS user_seko_id,
u.seko_limit AS user_seko_limit,
s.id AS supplier_id,
s.code AS supplier_code,
s.name AS supplier_name,
s.name_kana AS supplier_name_kana,
s.kind AS supplier_kind
FROM
R_DATA r
JOIN
M_USER u ON r.data_id = u.id
JOIN
M_SUPPLIER s ON r.relation_id = s.id
WHERE
u.status = '1'
AND
u.seko_id is not null
AND
u.seko_limit > now()
AND
s.kind = '4'
AND
u.login_id = ?
AND
u.password = ?
`
// const sql = 'SELECT * FROM M_USER'
const data = (await executeQuery(sql, [loginId, pwd])) as any[]
console.log('🚀 ~ POST ~ data:', data)
if (data.length > 0) {
console.log('start session edit!')
session.langCd = null
session.currPage = null
session.rowCount = null
session.startRow = null
session.endRow = null
session.compCd = null
session.agencyStoreId = null
session.storeId = data[0].supplier_code
session.storeNm = data[0].supplier_name
session.userId = data[0].user_login_id
session.category = data[0].supplier_name
session.userNm = `${data[0].user_sei} ${data[0].user_mei}`
session.userNmKana = `${data[0].user_sei_kana} ${data[0].user_mei_kana}`
session.telNo = data[0].tel
session.fax = data[0].fax
session.email = data[0].user_login_id
session.lastEditUser = null
session.storeGubun = null
session.pwCurr = null
session.pwdInitYn = null
session.apprStatCd = null
session.loginFailCnt = null
session.loginFailMinYn = null
session.priceViewStatCd = null
session.groupId = null
session.storeLvl = null
session.custCd = null
session.builderNo = data[0].user_seko_id
session.isLoggedIn = true
session.role = 'Partner'
console.log('end session edit!')
await session.save()
}
// qsp 유저 데이터 모양과 맞춰서 변환
const result = {
LANG_CD: null,
CURR_PAGE: null,
ROW_COUNT: null,
START_ROW: null,
END_ROW: null,
COMP_CD: null,
AGENCY_STORE_ID: null,
STORE_ID: data[0].supplier_code,
STORE_NM: data[0].supplier_name,
USER_ID: data[0].user_login_id,
CATEGORY: data[0].supplier_name,
USER_NM: `${data[0].user_sei} ${data[0].user_mei}`,
USER_NM_KANA: `${data[0].user_sei_kana} ${data[0].user_mei_kana}`,
TEL_NO: data[0].tel,
FAX: data[0].fax,
EMAIL: data[0].user_login_id,
LAST_EDIT_USER: null,
STORE_GUBUN: null,
PW_CURR: null,
PWD_INIT_YN: null,
APPR_STAT_CD: null,
LOGIN_FAIL_CNT: null,
LOGIN_FAIL_MIN_YN: null,
PRICE_VIEW_STAT_CD: null,
GROUP_ID: null,
STORE_LVL: null,
CUST_CD: null,
BUILDER_NO: data[0].user_seko_id,
}
return NextResponse.json({ code: 200, message: 'Partner Login is Succecss!!', result })
}

View File

@ -1,16 +0,0 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/libs/prisma'
export async function GET() {
// @ts-ignore
const roofMaterialCategory = await prisma.MS_SUITABLE.findMany({
select: {
roof_material: true,
},
distinct: ['roof_material'],
orderBy: {
roof_material: 'asc',
},
})
return NextResponse.json(roofMaterialCategory)
}

View File

@ -1,17 +0,0 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/libs/prisma'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const roofMaterial = searchParams.get('roof-material')
console.log('🚀 ~ GET ~ roof-material:', roofMaterial)
// @ts-ignore
const suitables = await prisma.MS_SUITABLE.findMany({
where: {
roof_material: roofMaterial,
},
})
return NextResponse.json(suitables)
}

View File

@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/libs/prisma'
import { SUITABLE_HEAD_CODE, type SuitableMain } from '@/types/Suitable'
export async function GET(request: NextRequest) {
try {
@ -7,26 +8,75 @@ export async function GET(request: NextRequest) {
const category = searchParams.get('category')
const keyword = searchParams.get('keyword')
let whereCondition: any = {}
let MainWhereCondition: any = {}
const whereCondition: string[] = []
const params: string[] = []
if (category) {
whereCondition['roof_material'] = category
whereCondition.push(`${SUITABLE_HEAD_CODE.ROOF_MT_CD} = @P1`)
params.push(category)
MainWhereCondition[SUITABLE_HEAD_CODE.ROOF_MT_CD] = category
}
if (keyword) {
whereCondition['product_name'] = {
whereCondition.push('PRODUCT_NAME LIKE @P2')
params.push(`%${keyword}%`)
MainWhereCondition['PRODUCT_NAME'] = {
contains: keyword,
}
}
console.log('🚀 ~ /api/suitable/list: ~ prisma where condition:', whereCondition)
const startTime = performance.now()
console.log(`쿼리 (main table) 시작 시간: ${startTime}ms`)
// @ts-ignore
const suitables = await prisma.MS_SUITABLE.findMany({
where: whereCondition,
const suitable = await prisma.MS_SUITABLE_MAIN.findMany({
select: {
ID: true,
PRODUCT_NAME: true,
ROOF_MT_CD: true,
},
where: MainWhereCondition,
orderBy: {
product_name: 'asc',
PRODUCT_NAME: 'asc',
},
})
return NextResponse.json(suitables)
const endTime = performance.now()
console.log(`쿼리 (main table) 종료 시간: ${endTime - startTime}ms`)
const mainIds: number[] = suitable.map((item: SuitableMain) => item.id)
const startTime2 = performance.now()
console.log(`쿼리 (detail table) 시작 시간: ${startTime2}ms`)
let detailQuery = `
SELECT
msd.main_id
, (
SELECT
msd_json.id
, msd_json.trestle_mfpc_cd
, msd_json.trestle_manufacturer_product_name
, msd_json.memo
FROM ms_suitable_detail msd_json
WHERE msd.main_id = msd_json.main_id
FOR JSON PATH
) AS detail
FROM ms_suitable_detail msd
-- WHERE 1=1
GROUP BY msd.main_id
`
if (whereCondition.length > 0) {
detailQuery = detailQuery.replace('-- WHERE 1=1', `WHERE msd.main_id IN @P1`)
}
// @ts-ignore
const detail = await prisma.$queryRawUnsafe(detailQuery, ...mainIds)
const endTime2 = performance.now()
console.log(`쿼리 (detail table) 종료 시간: ${endTime2 - startTime2}ms`)
const endTime3 = performance.now()
console.log(`쿼리 총 실행 시간: ${endTime3 - startTime}ms`)
return NextResponse.json({ suitable, detail })
} catch (error) {
console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error)
return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 })

View File

@ -0,0 +1,71 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/libs/prisma'
import { SUITABLE_HEAD_CODE, type Suitable } from '@/types/Suitable'
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const category = searchParams.get('category')
const keyword = searchParams.get('keyword')
const whereCondition: string[] = []
const params: string[] = []
if (category) {
whereCondition.push(`${SUITABLE_HEAD_CODE.ROOF_MT_CD} = @P1`)
params.push(category)
}
if (keyword) {
whereCondition.push('PRODUCT_NAME LIKE @P2')
params.push(`%${keyword}%`)
}
const startTime = performance.now()
console.log(`쿼리 시작 시간: ${startTime}ms`)
let query = `
SELECT
msm.id
, msm.product_name
, 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
, (
SELECT
msd_json.id
, msd_json.trestle_mfpc_cd
, msd_json.trestle_manufacturer_product_name
, msd_json.memo
FROM ms_suitable_detail msd_json
WHERE msd.main_id = msd_json.main_id
FOR JSON PATH
) AS detail
FROM ms_suitable_detail msd
GROUP BY msd.main_id
) AS details
ON msm.id = details.main_id
-- AND details.main_id IN (#mainIds)
-- WHERE 1=1
ORDER BY msm.product_name`
// 검색 조건 추가
if (whereCondition.length > 0) {
query = query.replace('-- WHERE 1=1', `WHERE ${whereCondition.join(' AND ')}`)
}
// @ts-ignore
const suitable: Suitable[] = await prisma.$queryRawUnsafe(query, ...params)
const endTime = performance.now()
console.log(`쿼리 실행 시간: ${endTime - startTime}ms`)
return NextResponse.json(suitable)
} catch (error) {
console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error)
return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 })
}
}

View File

@ -22,7 +22,7 @@ interface RootLayoutProps {
header: ReactNode
footer: ReactNode
floatBtn: ReactNode
}6
}
export default async function RootLayout({ children, header, footer, floatBtn }: RootLayoutProps): Promise<ReactNode> {
const cookieStore = await cookies()

View File

@ -0,0 +1,24 @@
import type { ReactNode } from 'react'
interface SuitableLayoutProps {
children: ReactNode
}
export default function layout({ children }: SuitableLayoutProps) {
return (
<>
<div className="container">
<div className="sale-contents">
<div className="border-frame">
<div className="pw-guide">
<div className="pw-guide-txt">使.</div>
<div className="pw-guide-txt">11.</div>
<div className="pw-guide-txt">or屋根材名を直接入力してください.</div>
</div>
</div>
{children}
</div>
</div>
</>
)
}

View File

@ -0,0 +1,9 @@
import SuitableRaw from '@/components/suitable/SuitableRaw'
export default function page() {
return (
<>
<SuitableRaw />
</>
)
}

View File

@ -48,7 +48,14 @@ export default function Login() {
} = useQuery<LoginData, Error>({
queryKey: ['login', 'account'],
queryFn: async () => {
const { data } = await axiosInstance('').post<LoginData>(`/api/auth`, {
let url = ''
if (!isPartners) {
url = '/api/auth'
} else {
url = '/api/partner'
}
const { data } = await axiosInstance('').post<LoginData>(`${url}`, {
loginId: account.loginId,
pwd: account.pwd,
})
@ -68,6 +75,7 @@ export default function Login() {
indivisualData: account.pwd,
})
// 세션 정보 저장
console.log('🚀 ~ Login ~ loginData:', loginData)
setSession({
...session,
...loginData?.result,

View File

@ -1,28 +1,70 @@
'use client'
import { useState } from 'react'
import SuitableCheckData from './SuitableCheckData'
import SuitableNoData from './SuitableNoData'
import Image from 'next/image'
import { useEffect, useState } from 'react'
import SuitableList 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() {
const [reference, setReference] = useState(false)
const [reference, setReference] = useState(true)
const { getSuitableCommCode, refetchBySearch } = useSuitable()
const { suitableCommCode, selectedCategory, setSelectedCategory, searchValue, setSearchValue, setIsSearch, clearSelectedItems } = useSuitableStore()
const handleInputSearch = async () => {
if (!searchValue.trim()) {
alert('屋根材の製品名を入力してください。')
return
}
setIsSearch(true)
refetchBySearch()
}
const handleInputClear = () => {
setSearchValue('')
setIsSearch(false)
refetchBySearch()
}
useEffect(() => {
refetchBySearch()
}, [selectedCategory])
useEffect(() => {
getSuitableCommCode()
return () => {
setSelectedCategory('')
setSearchValue('')
clearSelectedItems()
}
}, [])
return (
<div className="border-frame">
<div className="sale-form-bx">
<select className="select-form" name="" id="">
<option value="">.</option>
<option value="">.</option>
<option value="">.</option>
<option value="">.</option>
<select className="select-form" name="" id="" value={selectedCategory || ''} onChange={(e) => setSelectedCategory(e.target.value)}>
<option value="">.</option>
{suitableCommCode.get(SUITABLE_HEAD_CODE.ROOF_MT_CD)?.map((category: CommCode, index: number) => (
<option key={index} value={category.code}>
{category.codeJp}
</option>
))}
</select>
</div>
<div className="sale-form-bx">
<div className="search-input">
<input type="text" className="search-frame" placeholder="屋根材 製品名を入力してください." />
<button className="search-icon"></button>
<input
type="text"
className="search-frame"
placeholder="屋根材 製品名を入力してください."
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
/>
{searchValue && <button className="del-icon" onClick={handleInputClear} />}
<button className="search-icon" onClick={handleInputSearch} />
</div>
</div>
<div className="compliance-check-wrap">
@ -68,37 +110,8 @@ export default function Suitable() {
</li>
</ul>
</div>
{/* checkData */}
{/* 데이터 없을경우 버튼 영역 안보여야함 */}
<SuitableCheckData />
<SuitableCheckData />
<SuitableCheckData />
<SuitableCheckData />
{/* 데이터 없을경우 버튼 영역 안보여야함 */}
<div className="float-btn-wrap">
<div className="btn-flex-wrap com">
<div className="btn-bx">
<button className="btn-frame n-blue icon">
<i className="btn-arr"></i>
</button>
</div>
<div className="btn-bx">
<button className="btn-frame red icon">
<i className="btn-arr"></i>
</button>
</div>
<div className="btn-bx">
<button className="btn-frame n-blue icon">
<i className="btn-arr"></i>
</button>
</div>
</div>
</div>
<SuitableList />
</div>
{/* 검색기록 없을떄 위에 두 영역 안보이고 이 부분만 보여야 함*/}
{/* <SuitableNoData /> */}
</div>
)
}

View File

@ -0,0 +1,25 @@
'use client'
export default function SuitableButton() {
return (
<div className="float-btn-wrap">
<div className="btn-flex-wrap com">
<div className="btn-bx">
<button className="btn-frame n-blue icon">
<i className="btn-arr"></i>
</button>
</div>
<div className="btn-bx">
<button className="btn-frame red icon">
<i className="btn-arr"></i>
</button>
</div>
<div className="btn-bx">
<button className="btn-frame n-blue icon">
<i className="btn-arr"></i>
</button>
</div>
</div>
</div>
)
}

View File

@ -1,81 +0,0 @@
'use client'
import Image from 'next/image'
export default function SuitableCheckData() {
return (
<>
<div className={`compliance-check-bx act`}>
<div className="check-name-wrap">
<div className="check-form-box ch-bld">
<input type="checkbox" id="ch01" />
<label htmlFor="ch01"></label>
</div>
<div className="check-name-btn">
<button className="bx-btn"></button>
</div>
</div>
<ul className="reference-list check">
<li className="reference-item">
<div className="check-item-wrap">
<div className="check-form-box light">
<input type="checkbox" id="ch02" />
<label htmlFor="ch02"></label>
</div>
<div className="compliance-icon-wrap">
<div className="compliance-icon">
<Image src={'/assets/images/sub/compliance_x_icon.svg'} width={22} height={22} alt=""></Image>
</div>
</div>
</div>
</li>
<li className="reference-item">
<div className="check-item-wrap">
<div className="check-form-box light">
<input type="checkbox" id="ch03" />
<label htmlFor="ch03"> </label>
</div>
<div className="compliance-icon-wrap">
<div className="compliance-icon">
<Image src={'/assets/images/sub/compliance_x_icon.svg'} width={22} height={22} alt=""></Image>
</div>
<div className="compliance-icon">
<Image src={'/assets/images/sub/compliance_tip_icon.svg'} width={22} height={22} alt=""></Image>
</div>
</div>
</div>
</li>
<li className="reference-item">
<div className="check-item-wrap">
<div className="check-form-box light">
<input type="checkbox" id="ch04" />
<label htmlFor="ch04"></label>
</div>
<div className="compliance-icon-wrap">
<div className="compliance-icon">
<Image src={'/assets/images/sub/compliance_quest_icon.svg'} width={22} height={22} alt=""></Image>
</div>
<div className="compliance-icon">
<Image src={'/assets/images/sub/compliance_tip_icon.svg'} width={22} height={22} alt=""></Image>
</div>
</div>
</div>
</li>
<li className="reference-item">
<div className="check-item-wrap">
<div className="check-form-box light">
<input type="checkbox" id="ch05" />
<label htmlFor="ch05"></label>
</div>
<div className="compliance-icon-wrap">
<div className="compliance-icon">
<Image src={'/assets/images/sub/compliance_check_icon.svg'} width={22} height={22} alt=""></Image>
</div>
</div>
</div>
</li>
</ul>
</div>
</>
)
}

View File

@ -0,0 +1,174 @@
'use client'
import Image from 'next/image'
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 SuitableMain, type SuitableDetail } from '@/types/Suitable'
// 한 번에 로드할 아이템 수
const ITEMS_PER_PAGE = 100
export default function SuitableList() {
const { toCodeName, suitableSearchResults, isSearchLoading, toSuitableDetail } = useSuitable()
const { selectedItems, addSelectedItem, removeSelectedItem } = useSuitableStore()
const [openItems, setOpenItems] = useState<Set<number>>(new Set())
const [visibleItems, setVisibleItems] = useState<SuitableMain[]>([])
const [page, setPage] = useState(1)
const [isLoadingMore, setIsLoadingMore] = useState(false)
const observerTarget = useRef<HTMLDivElement>(null)
// 선택된 아이템 확인 함수 메모이제이션
const isItemSelected = useCallback(
(itemId: number) => {
return selectedItems.some((selected) => selected === itemId)
},
[selectedItems],
)
// 초기 데이터 로드
useEffect(() => {
if (suitableSearchResults) {
const initialItems = suitableSearchResults.suitable.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.suitable.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(
(itemId: number) => {
isItemSelected(itemId) ? removeSelectedItem(itemId) : addSelectedItem(itemId)
},
[isItemSelected, addSelectedItem, removeSelectedItem],
)
const toggleItemOpen = useCallback((itemId: number) => {
setOpenItems((prev) => {
const newOpenItems = new Set(prev)
newOpenItems.has(itemId) ? newOpenItems.delete(itemId) : newOpenItems.add(itemId)
return newOpenItems
})
}, [])
// TODO: 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요
const suitableCheck = useCallback((value: string) => {
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>
)
}
}, [])
// 메모이제이션된 아이템 렌더링
const renderItem = useCallback(
(item: SuitableMain) => {
const isSelected = isItemSelected(item.id)
const isOpen = openItems.has(item.id)
return (
<div className={`compliance-check-bx ${isOpen ? 'act' : ''}`} key={item.id}>
<div className="check-name-wrap">
<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">
<button className="bx-btn" onClick={() => toggleItemOpen(item.id)}></button>
</div>
</div>
<ul className="reference-list check">
{toSuitableDetail(item.id).map((subItem: SuitableDetail) => (
<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}`} />
<label htmlFor={`ch${subItem.id}`}>{toCodeName(SUITABLE_HEAD_CODE.TRESTLE_MFPC_CD, subItem.trestleMfpcCd)}</label>
</div>
<div className="compliance-icon-wrap">
{suitableCheck(subItem.trestleManufacturerProductName)}
{subItem.memo && (
<div className="compliance-icon">
<Image src={'/assets/images/sub/compliance_tip_icon.svg'} width={22} height={22} alt=""></Image>
</div>
)}
</div>
</div>
</li>
))}
</ul>
</div>
)
},
[isItemSelected, openItems, handleItemClick, toggleItemOpen, suitableCheck, toCodeName, toSuitableDetail],
)
// 메모이제이션된 아이템 리스트
const renderedItems = useMemo(() => {
return visibleItems.map(renderItem)
}, [visibleItems, renderItem])
if (isSearchLoading) {
return <div>Loading...</div>
}
if (!suitableSearchResults?.suitable.length) {
return <SuitableNoData />
}
return (
<>
{renderedItems}
<div ref={observerTarget} className="loading-indicator">
{isLoadingMore && <div className="loading-more"> ...</div>}
</div>
<SuitableButton />
</>
)
}

View File

@ -0,0 +1,173 @@
'use client'
import Image from 'next/image'
import { useState, useEffect, useRef, useCallback, useMemo } from 'react'
import SuitableButton from './SuitableButton'
import SuitableNoData from './SuitableNoData'
import { useSuitableRaw, type Suitable } from '@/hooks/useSuitableRaw'
import { useSuitableStore } from '@/store/useSuitableStore'
import { SUITABLE_HEAD_CODE, type SuitableDetail } from '@/types/Suitable'
// 한 번에 로드할 아이템 수
const ITEMS_PER_PAGE = 100
export default function SuitableListRaw() {
const { toCodeName, suitableSearchResults, isSearchLoading, toSuitableDetail } = useSuitableRaw()
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)
// 선택된 아이템 확인 함수 메모이제이션
const isItemSelected = useCallback(
(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(
(itemId: number) => {
isItemSelected(itemId) ? removeSelectedItem(itemId) : addSelectedItem(itemId)
},
[isItemSelected, addSelectedItem, removeSelectedItem],
)
const toggleItemOpen = useCallback((itemId: number) => {
setOpenItems((prev) => {
const newOpenItems = new Set(prev)
newOpenItems.has(itemId) ? newOpenItems.delete(itemId) : newOpenItems.add(itemId)
return newOpenItems
})
}, [])
// TODO: 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요
const suitableCheck = useCallback((value: string) => {
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>
)
}
}, [])
// 메모이제이션된 아이템 렌더링
const renderItem = useCallback(
(item: Suitable) => {
const isSelected = isItemSelected(item.id)
const isOpen = openItems.has(item.id)
return (
<div className={`compliance-check-bx ${isOpen ? 'act' : ''}`} key={item.id}>
<div className="check-name-wrap">
<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">
<button className="bx-btn" onClick={() => toggleItemOpen(item.id)}></button>
</div>
</div>
<ul className="reference-list check">
{toSuitableDetail(item.detail).map((subItem: SuitableDetail) => (
<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}`} />
<label htmlFor={`ch${subItem.id}`}>{toCodeName(SUITABLE_HEAD_CODE.TRESTLE_MFPC_CD, subItem.trestleMfpcCd)}</label>
</div>
<div className="compliance-icon-wrap">
{suitableCheck(subItem.trestleManufacturerProductName)}
{subItem.memo && (
<div className="compliance-icon">
<Image src={'/assets/images/sub/compliance_tip_icon.svg'} width={22} height={22} alt=""></Image>
</div>
)}
</div>
</div>
</li>
))}
</ul>
</div>
)
},
[isItemSelected, openItems, handleItemClick, toggleItemOpen, suitableCheck, toCodeName, toSuitableDetail],
)
// 메모이제이션된 아이템 리스트
const renderedItems = useMemo(() => {
return visibleItems.map(renderItem)
}, [visibleItems, renderItem])
if (isSearchLoading) {
return <div>Loading...</div>
}
if (!suitableSearchResults?.length) {
return <SuitableNoData />
}
return (
<>
{renderedItems}
<div ref={observerTarget} className="loading-indicator">
{isLoadingMore && <div className="loading-more"> ...</div>}
</div>
<SuitableButton />
</>
)
}

View File

@ -0,0 +1,118 @@
'use client'
import Image from 'next/image'
import { useEffect, useState } from 'react'
import SuitableListRaw from './SuitableListRaw'
import { useSuitableRaw } from '@/hooks/useSuitableRaw'
import { useSuitableStore } from '@/store/useSuitableStore'
import type { CommCode } from '@/types/CommCode'
import { SUITABLE_HEAD_CODE } from '@/types/Suitable'
export default function SuitableRaw() {
const [reference, setReference] = useState(true)
const { getSuitableCommCode, refetchBySearch } = useSuitableRaw()
const { suitableCommCode, selectedCategory, setSelectedCategory, searchValue, setSearchValue, setIsSearch, clearSelectedItems } = useSuitableStore()
const handleInputSearch = async () => {
if (!searchValue.trim()) {
alert('屋根材の製品名を入力してください。')
return
}
setIsSearch(true)
refetchBySearch()
}
const handleInputClear = () => {
setSearchValue('')
setIsSearch(false)
refetchBySearch()
}
useEffect(() => {
refetchBySearch()
}, [selectedCategory])
useEffect(() => {
getSuitableCommCode()
return () => {
setSelectedCategory('')
setSearchValue('')
clearSelectedItems()
}
}, [])
return (
<div className="border-frame">
<span>1 </span>
<div className="sale-form-bx">
<select className="select-form" name="" id="" value={selectedCategory || ''} onChange={(e) => setSelectedCategory(e.target.value)}>
<option value="">.</option>
{suitableCommCode.get(SUITABLE_HEAD_CODE.ROOF_MT_CD)?.map((category: CommCode, index: number) => (
<option key={index} value={category.code}>
{category.codeJp}
</option>
))}
</select>
</div>
<div className="sale-form-bx">
<div className="search-input">
<input
type="text"
className="search-frame"
placeholder="屋根材 製品名を入力してください."
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
/>
{searchValue && <button className="del-icon" onClick={handleInputClear} />}
<button className="search-icon" onClick={handleInputSearch} />
</div>
</div>
<div className="compliance-check-wrap">
<div className={`compliance-check-bx ${reference ? 'act' : ''}`}>
<div className="check-name-wrap">
<div className="check-name"></div>
<div className="check-name-btn">
<button className="bx-btn" onClick={() => setReference(!reference)}></button>
</div>
</div>
<ul className="reference-list">
<li className="reference-item">
<div className="reference-item-bx">
<div className="compliance-icon">
<Image src={'/assets/images/sub/compliance_check_icon.svg'} width={22} height={22} alt=""></Image>
</div>
<span></span>
</div>
</li>
<li className="reference-item">
<div className="reference-item-bx">
<div className="compliance-icon">
<Image src={'/assets/images/sub/compliance_x_icon.svg'} width={22} height={22} alt=""></Image>
</div>
<span></span>
</div>
</li>
<li className="reference-item">
<div className="reference-item-bx">
<div className="compliance-icon">
<Image src={'/assets/images/sub/compliance_quest_icon.svg'} width={22} height={22} alt=""></Image>
</div>
<span></span>
</div>
</li>
<li className="reference-item">
<div className="reference-item-bx">
<div className="compliance-icon">
<Image src={'/assets/images/sub/compliance_tip_icon.svg'} width={22} height={22} alt=""></Image>
</div>
<span></span>
</div>
</li>
</ul>
</div>
<SuitableListRaw />
</div>
</div>
)
}

18
src/hooks/useCommCode.ts Normal file
View File

@ -0,0 +1,18 @@
import { axiosInstance } from '@/libs/axios'
import type { CommCode } from '@/types/CommCode'
export function useCommCode() {
const getCommCode = async (headCode: string): Promise<CommCode[]> => {
try {
const response = await axiosInstance(null).get<CommCode[]>('/api/comm-code', { params: { headCode: headCode } })
return response.data
} catch (error) {
console.error(`common code (${headCode}) load failed:`, error)
return []
}
}
return {
getCommCode,
}
}

View File

@ -1,30 +1,107 @@
import { suitableApi } from '@/api/suitable'
import { useQuery } from '@tanstack/react-query'
import { axiosInstance, transformObjectKeys } from '@/libs/axios'
import { useSuitableStore } from '@/store/useSuitableStore'
import { useCommCode } from './useCommCode'
import { SUITABLE_HEAD_CODE, type SuitableDetailGroup, type SuitableMain, type Suitable, type SuitableDetail } from '@/types/Suitable'
export function useSuitable() {
const getCategories = async () => {
const { getCommCode } = useCommCode()
const { selectedCategory, searchValue, suitableCommCode, setSuitableCommCode, isSearch } = useSuitableStore()
const getSuitables = async (): Promise<Suitable> => {
try {
// return await suitableApi.getCategory()
const response = await axiosInstance(null).get<Suitable>('/api/suitable/list')
return response.data
} catch (error) {
console.error('카테고리 데이터 로드 실패:', error)
console.error('지붕재 데이터 로드 실패:', error)
return { suitable: [], detail: [] }
}
}
// 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) {
getCommCode(code).then((res) => {
setSuitableCommCode(code, res)
})
}
}
const toCodeName = (headCode: string, code: string): string => {
const commCode = suitableCommCode.get(headCode)
return commCode?.find((item) => item.code === code)?.codeJp || ''
}
const toSuitableDetail = (mainId: number): SuitableDetail[] => {
try {
const suitableDetailString = suitableList?.detail.find((item) => item.mainId === mainId)?.detail
if (!suitableDetailString) {
return []
}
const suitableDetailArray = transformObjectKeys(JSON.parse(suitableDetailString)) as SuitableDetail[]
if (!Array.isArray(suitableDetailArray)) {
throw new Error('suitableDetailArray is not an array')
}
return suitableDetailArray
} catch (error) {
console.error('지붕재 데이터 파싱 실패:', error)
return []
}
}
const getSuitables = async () => {
try {
// return await suitableApi.getList()
} catch (error) {
console.error('지붕재 데이터 로드 실패:', error)
}
}
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 updateSearchResults = async (selectedCategory: string | undefined, searchValue: string | undefined) => {
try {
// return await suitableApi.getList(selectedCategory, searchValue)
} catch (error) {
console.error('지붕재 데이터 검색 실패:', error)
}
}
const {
data: suitableSearchResults,
refetch: refetchBySearch,
isLoading: isSearchLoading,
} = useQuery<Suitable>({
queryKey: ['suitables', 'search', selectedCategory, isSearch],
queryFn: async () => {
if (!isSearch && !selectedCategory) {
// 검색 상태가 아니면 초기 데이터 반환 임시처리
return isInitialLoading ? await getSuitables() : suitableList ?? { suitable: [], detail: [] }
} else {
const filteredSuitable = suitableList?.suitable.filter((item: SuitableMain) => {
const categoryMatch = !selectedCategory || item.roofMtCd === selectedCategory
const searchMatch = !searchValue || item.productName.includes(searchValue)
return categoryMatch && searchMatch
}) ?? []
const mainIds = filteredSuitable.map((item: SuitableMain) => item.id)
const filteredDetail = suitableList?.detail.filter((item: SuitableDetailGroup) => {
return mainIds.includes(item.mainId)
}) ?? []
return { suitable: filteredSuitable, detail: filteredDetail }
}
},
staleTime: 1000 * 60 * 10,
gcTime: 1000 * 60 * 10,
enabled: true,
})
return { getCategories, getSuitables, updateSearchResults }
return {
getSuitables,
getSuitableCommCode,
toCodeName,
toSuitableDetail,
suitableList,
suitableSearchResults,
refetchBySearch,
isSearchLoading,
}
}

109
src/hooks/useSuitableRaw.ts Normal file
View File

@ -0,0 +1,109 @@
import { useQuery } from '@tanstack/react-query'
import { axiosInstance, transformObjectKeys } from '@/libs/axios'
import { useSuitableStore } from '@/store/useSuitableStore'
import { useCommCode } from './useCommCode'
import { SUITABLE_HEAD_CODE, type SuitableDetail } from '@/types/Suitable'
export type Suitable = {
id: number
productName: string
manuFtCd: string
roofMtCd: string
roofShCd: string
detail: string
}
export function useSuitableRaw() {
const { getCommCode } = useCommCode()
const { selectedCategory, searchValue, suitableCommCode, setSuitableCommCode, isSearch } = useSuitableStore()
const getSuitables = async (): Promise<Suitable[]> => {
try {
const response = await axiosInstance(null).get<Suitable[]>('/api/suitable/list/test')
return response.data
} catch (error) {
console.error('지붕재 데이터 로드 실패:', error)
return []
}
}
// 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) {
getCommCode(code).then((res) => {
setSuitableCommCode(code, res)
})
}
}
const toCodeName = (headCode: string, code: string): string => {
const commCode = suitableCommCode.get(headCode)
return commCode?.find((item) => item.code === code)?.codeJp || ''
}
const toSuitableDetail = (suitableDetailString: string): SuitableDetail[] => {
try {
const suitableDetailArray = transformObjectKeys(JSON.parse(suitableDetailString)) as SuitableDetail[]
if (!Array.isArray(suitableDetailArray)) {
throw new Error('suitableDetailArray is not an array')
}
return suitableDetailArray
} catch (error) {
console.error('지붕재 데이터 파싱 실패:', error)
return []
}
}
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: suitableSearchResults,
refetch: refetchBySearch,
isLoading: isSearchLoading,
// } = useQuery<SuitableData>({
} = 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
}) ?? []
)
}
},
staleTime: 1000 * 60 * 10,
gcTime: 1000 * 60 * 10,
enabled: true,
})
return {
getSuitables,
getSuitableCommCode,
toCodeName,
toSuitableDetail,
suitableList,
suitableSearchResults,
refetchBySearch,
isSearchLoading,
}
}

View File

@ -68,7 +68,7 @@ export const transferResponse = (response: any) => {
}
// camel case object 반환
const transformObjectKeys = (obj: any): any => {
export const transformObjectKeys = (obj: any): any => {
if (Array.isArray(obj)) {
return obj.map(transformObjectKeys)
}

38
src/libs/partner.tsx Normal file
View File

@ -0,0 +1,38 @@
import { createPool } from 'mysql2'
const pool = createPool({
host: process.env.DB_HOST as string,
user: process.env.DB_USER as string,
password: process.env.DB_PASSWORD as string,
database: process.env.DB_DATABASE as string,
port: Number(process.env.DB_PORT),
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
})
pool.getConnection((err, conn) => {
if (err) console.log('Error connecting to db...')
else console.log('Connected to db...!')
conn.release()
})
const executeQuery = (query: string, arrParams: any[]) => {
return new Promise((resolve, reject) => {
try {
pool.query(query, arrParams, (err, data) => {
if (err) {
console.log('🚀 ~ pool.query ~ err:', err)
reject(err)
}
console.log('🚀 ~ pool.query ~ data:', data)
resolve(data)
})
} catch (err) {
console.log('🚀 ~ returnnewPromise ~ err:', err)
reject(err)
}
})
}
export default executeQuery

View File

@ -1,12 +1,13 @@
'use client'
import { useEffect } from 'react'
import { usePathname } from 'next/navigation'
import { usePathname, useRouter } from 'next/navigation'
import { useHeaderStore } from '@/store/header'
import { usePopupController } from '@/store/popupController'
import { useSideNavState } from '@/store/sideNavState'
import { useSessionStore } from '@/store/session'
import { tracking } from '@/libs/tracking'
declare global {
interface Window {
@ -21,12 +22,30 @@ interface EdgeProviderProps {
}
export default function EdgeProvider({ children, sessionData }: EdgeProviderProps) {
const router = useRouter()
const pathname = usePathname()
const { setBackBtn } = useHeaderStore()
const { reset } = useSideNavState()
const { setAlertMsg, setAlertBtn, setAlert, setAlert2, setAlert2BtnYes, setAlert2BtnNo } = usePopupController()
const { session, setSession } = useSessionStore()
if (pathname === '/login') {
if (session?.isLoggedIn) {
router.push('/')
}
}
/**
*
*
*/
const handlePageEvent = (path: string) => {
tracking({
url: path,
data: '',
})
}
/**
* alert - window.alert
* @param msg
@ -88,6 +107,8 @@ export default function EdgeProvider({ children, sessionData }: EdgeProviderProp
}
//사이드바 초기화
reset()
// 페이지 이벤트 트래킹
// handlePageEvent(pathname)
}, [pathname])
return <>{children}</>

View File

@ -1,68 +1,71 @@
import { create } from 'zustand'
import { Suitable, suitableApi } from '@/api/suitable'
import type { CommCode } from '@/types/CommCode'
interface SuitableState {
// // 검색 결과 리스트
// searchResults: Suitable[]
// // 초기 데이터 로드
// fetchInitializeData: () => Promise<void>
// // 검색 결과 설정
// setSearchResults: (results: Suitable[]) => void
// // 검색 결과 초기화
// resetSearchResults: () => void
/* 공통코드 */
suitableCommCode: Map<string, CommCode[]>
/* 공통코드 설정 */
setSuitableCommCode: (headCode: string, commCode: CommCode[]) => void
// 선택된 아이템 리스트
selectedItems: Suitable[]
// 선택된 아이템 추가
addSelectedItem: (item: Suitable) => void
// 선택된 아이템 제거
/* 검색 상태 */
isSearch: boolean
/* 검색 상태 설정 */
setIsSearch: (isSearch: boolean) => void
/* 선택된 카테고리 */
selectedCategory: string
/* 선택된 카테고리 설정 */
setSelectedCategory: (category: string) => void
/* 검색 값 */
searchValue: string
/* 검색 값 설정 */
setSearchValue: (value: string) => void
/* 선택된 아이템 리스트 */
selectedItems: number[]
/* 선택된 아이템 추가 */
addSelectedItem: (itemId: number) => void
/* 선택된 아이템 제거 */
removeSelectedItem: (itemId: number) => void
// 선택된 아이템 모두 제거
/* 선택된 아이템 모두 제거 */
clearSelectedItems: () => void
}
export const useSuitableStore = create<SuitableState>((set) => ({
// // 초기 상태
// searchResults: [],
suitableCommCode: new Map() as Map<string, CommCode[]>,
isSearch: false as boolean,
selectedCategory: '' as string,
searchValue: '' as string,
selectedItems: [] as number[],
// // 초기 데이터 로드
// fetchInitializeData: async () => {
// const suitables = await fetchInitialSuitablee()
// set({ searchResults: suitables })
// },
// // 검색 결과 설정
// setSearchResults: (results) => set({ searchResults: results }),
// // 검색 결과 초기화
// resetSearchResults: () => set({ searchResults: [] }),
// 초기 상태
selectedItems: [],
// 선택된 아이템 추가 (중복 방지)
addSelectedItem: (item) =>
/* 공통코드 설정 */
setSuitableCommCode: (headCode: string, commCode: CommCode[]) =>
set((state) => ({
selectedItems: state.selectedItems.some((i) => i.id === item.id) ? state.selectedItems : [...state.selectedItems, item],
suitableCommCode: new Map(state.suitableCommCode).set(headCode, commCode),
})),
// 선택된 아이템 제거
removeSelectedItem: (itemId) =>
/* 검색 상태 설정 */
setIsSearch: (isSearch: boolean) => set({ isSearch }),
/* 선택된 카테고리 설정 */
setSelectedCategory: (category: string) => set({ selectedCategory: category }),
/* 검색 값 설정 */
setSearchValue: (value: string) => set({ searchValue: value }),
/* 선택된 아이템 추가 */
addSelectedItem: (itemId: number) =>
set((state) => ({
selectedItems: state.selectedItems.filter((item) => item.id !== itemId),
selectedItems: state.selectedItems.some((i) => i === itemId) ? state.selectedItems : [...state.selectedItems, itemId],
})),
// 선택된 아이템 모두 제거
/* 선택된 아이템 제거 */
removeSelectedItem: (itemId: number) =>
set((state) => ({
selectedItems: state.selectedItems.filter((i) => i !== itemId),
})),
/* 선택된 아이템 모두 제거 */
clearSelectedItems: () => set({ selectedItems: [] }),
}))
// // 전체 데이터 초기화 함수
// async function fetchInitialSuitablee() {
// try {
// const suitable = await suitableApi.getList()
// return suitable
// } catch (error) {
// console.error('초기 데이터 로드 실패:', error)
// return []
// }
// }

5
src/types/CommCode.ts Normal file
View File

@ -0,0 +1,5 @@
export type CommCode = {
headCd: string
code: string
codeJp: string
}

44
src/types/Suitable.ts Normal file
View File

@ -0,0 +1,44 @@
export enum SUITABLE_HEAD_CODE {
/* 지붕재 제조사명 */
MANU_FT_CD = 'MANU_FT_CD',
/* 지붕재 종류 */
ROOF_MT_CD = 'ROOF_MT_CD',
/* 마운팅 브래킷 종류 */
ROOF_SH_CD = 'ROOF_SH_CD',
/* 마운팅 브래킷 제조사명 및 제품코드드 */
TRESTLE_MFPC_CD = 'TRESTLE_MFPC_CD',
}
export type SuitableMain = {
id: number
productName: string
manuFtCd: string
roofMtCd: string
roofShCd: string
}
export type SuitableDetail = {
id: number
mainId: number
trestleMfpcCd: string
trestleManufacturerProductName: string
memo: string
}
// export type Suitable = {
// id: number
// productName: string
// manuFtCd: string
// roofMtCd: string
// roofShCd: string
// detail: string
// }
export type SuitableDetailGroup = {
mainId: number
detail: string
}
export type Suitable = {
suitable: SuitableMain[]
detail: SuitableDetailGroup[]
}