diff --git a/.env.development b/.env.development index 75df0fa..1a372fa 100644 --- a/.env.development +++ b/.env.development @@ -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 \ No newline at end of file diff --git a/.env.production b/.env.production index e1a6d43..4c39e83 100644 --- a/.env.production +++ b/.env.production @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index 950676f..72a29f7 100644 --- a/README.md +++ b/README.md @@ -58,3 +58,20 @@ session에 있는 role 키로 구분한다 session.role === 'Partner' - 이외의 경우 -> 굳이 체크할 필요 없어보임\ session.role === 'User' + + +# 지붕재 적합성 TODO + +``` +const suitableCheck = (value: string) => { + if (value === '×') { + return + } else if (value === 'ー') { + return + } else { + return + } + } +``` + +- 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요 diff --git a/package.json b/package.json index 519fd8a..a57feca 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1875d7..9d972d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/app/api/auth/chg-pwd/route.ts b/src/app/api/auth/chg-pwd/route.ts index 436f101..71e9f6b 100644 --- a/src/app/api/auth/chg-pwd/route.ts +++ b/src/app/api/auth/chg-pwd/route.ts @@ -1,5 +1,4 @@ import { NextResponse } from 'next/server' - import { axiosInstance } from '@/libs/axios' export async function POST(req: Request) { diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts index 390f8d1..78f0a58 100644 --- a/src/app/api/auth/logout/route.ts +++ b/src/app/api/auth/logout/route.ts @@ -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() diff --git a/src/app/api/auth/route.ts b/src/app/api/auth/route.ts index 32ac8f9..8caef42 100644 --- a/src/app/api/auth/route.ts +++ b/src/app/api/auth/route.ts @@ -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() diff --git a/src/app/api/comm-code/route.ts b/src/app/api/comm-code/route.ts new file mode 100644 index 0000000..6b4852c --- /dev/null +++ b/src/app/api/comm-code/route.ts @@ -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 }) + } +} diff --git a/src/app/api/partner/route.ts b/src/app/api/partner/route.ts new file mode 100644 index 0000000..abb63df --- /dev/null +++ b/src/app/api/partner/route.ts @@ -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(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 }) +} diff --git a/src/app/api/suitable/category/route.ts b/src/app/api/suitable/category/route.ts deleted file mode 100644 index 288a74a..0000000 --- a/src/app/api/suitable/category/route.ts +++ /dev/null @@ -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) -} diff --git a/src/app/api/suitable/detail/route.ts b/src/app/api/suitable/detail/route.ts deleted file mode 100644 index d29f666..0000000 --- a/src/app/api/suitable/detail/route.ts +++ /dev/null @@ -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) -} diff --git a/src/app/api/suitable/list/route.ts b/src/app/api/suitable/list/route.ts index d789275..32eac33 100644 --- a/src/app/api/suitable/list/route.ts +++ b/src/app/api/suitable/list/route.ts @@ -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 }) diff --git a/src/app/api/suitable/list/test/route.ts b/src/app/api/suitable/list/test/route.ts new file mode 100644 index 0000000..e4688bd --- /dev/null +++ b/src/app/api/suitable/list/test/route.ts @@ -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 }) + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7ba05b0..508c340 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -22,7 +22,7 @@ interface RootLayoutProps { header: ReactNode footer: ReactNode floatBtn: ReactNode -}6 +} export default async function RootLayout({ children, header, footer, floatBtn }: RootLayoutProps): Promise { const cookieStore = await cookies() diff --git a/src/app/suitable-test/layout.tsx b/src/app/suitable-test/layout.tsx new file mode 100644 index 0000000..e5e7c3f --- /dev/null +++ b/src/app/suitable-test/layout.tsx @@ -0,0 +1,24 @@ +import type { ReactNode } from 'react' + +interface SuitableLayoutProps { + children: ReactNode +} + +export default function layout({ children }: SuitableLayoutProps) { + return ( + <> +
+
+
+
+
この適合表は参考資料として使用してください.
+
詳細やお問い合わせは1:1お問い合わせをご利用ください.
+
屋根材の選択or屋根材名を直接入力してください.
+
+
+ {children} +
+
+ + ) +} diff --git a/src/app/suitable-test/page.tsx b/src/app/suitable-test/page.tsx new file mode 100644 index 0000000..a5299fe --- /dev/null +++ b/src/app/suitable-test/page.tsx @@ -0,0 +1,9 @@ +import SuitableRaw from '@/components/suitable/SuitableRaw' + +export default function page() { + return ( + <> + + + ) +} diff --git a/src/components/Login.tsx b/src/components/Login.tsx index 5f086af..d5edc00 100644 --- a/src/components/Login.tsx +++ b/src/components/Login.tsx @@ -48,7 +48,14 @@ export default function Login() { } = useQuery({ queryKey: ['login', 'account'], queryFn: async () => { - const { data } = await axiosInstance('').post(`/api/auth`, { + let url = '' + if (!isPartners) { + url = '/api/auth' + } else { + url = '/api/partner' + } + + const { data } = await axiosInstance('').post(`${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, diff --git a/src/components/suitable/Suitable.tsx b/src/components/suitable/Suitable.tsx index f9f6615..36a397f 100644 --- a/src/components/suitable/Suitable.tsx +++ b/src/components/suitable/Suitable.tsx @@ -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 (
- setSelectedCategory(e.target.value)}> + {suitableCommCode.get(SUITABLE_HEAD_CODE.ROOF_MT_CD)?.map((category: CommCode, index: number) => ( + + ))}
- - + setSearchValue(e.target.value)} + /> + {searchValue &&
@@ -68,37 +110,8 @@ export default function Suitable() {
- {/* checkData */} - {/* 데이터 없을경우 버튼 영역 안보여야함 */} - - - - - - {/* 데이터 없을경우 버튼 영역 안보여야함 */} -
-
-
- -
-
- -
-
- -
-
-
+
- - {/* 검색기록 없을떄 위에 두 영역 안보이고 이 부분만 보여야 함*/} - {/* */} ) } diff --git a/src/components/suitable/SuitableButton.tsx b/src/components/suitable/SuitableButton.tsx new file mode 100644 index 0000000..f412c89 --- /dev/null +++ b/src/components/suitable/SuitableButton.tsx @@ -0,0 +1,25 @@ +'use client' + +export default function SuitableButton() { + return ( +
+
+
+ +
+
+ +
+
+ +
+
+
+ ) +} diff --git a/src/components/suitable/SuitableCheckData.tsx b/src/components/suitable/SuitableCheckData.tsx deleted file mode 100644 index 2a57c21..0000000 --- a/src/components/suitable/SuitableCheckData.tsx +++ /dev/null @@ -1,81 +0,0 @@ -'use client' - -import Image from 'next/image' - -export default function SuitableCheckData() { - return ( - <> -
-
-
- - -
-
- -
-
-
    -
  • -
    -
    - - -
    -
    -
    - -
    -
    -
    -
  • -
  • -
    -
    - - -
    -
    -
    - -
    -
    - -
    -
    -
    -
  • -
  • -
    -
    - - -
    -
    -
    - -
    -
    - -
    -
    -
    -
  • -
  • -
    -
    - - -
    -
    -
    - -
    -
    -
    -
  • -
-
- - ) -} diff --git a/src/components/suitable/SuitableList.tsx b/src/components/suitable/SuitableList.tsx new file mode 100644 index 0000000..18d94e5 --- /dev/null +++ b/src/components/suitable/SuitableList.tsx @@ -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>(new Set()) + const [visibleItems, setVisibleItems] = useState([]) + const [page, setPage] = useState(1) + const [isLoadingMore, setIsLoadingMore] = useState(false) + const observerTarget = useRef(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 ( +
+ +
+ ) + } else if (value === 'ー') { + return ( +
+ +
+ ) + } else { + return ( +
+ +
+ ) + } + }, []) + + // 메모이제이션된 아이템 렌더링 + const renderItem = useCallback( + (item: SuitableMain) => { + const isSelected = isItemSelected(item.id) + const isOpen = openItems.has(item.id) + + return ( +
+
+
+ handleItemClick(item.id)} /> + +
+
+ +
+
+
    + {toSuitableDetail(item.id).map((subItem: SuitableDetail) => ( +
  • +
    +
    + + +
    +
    + {suitableCheck(subItem.trestleManufacturerProductName)} + {subItem.memo && ( +
    + +
    + )} +
    +
    +
  • + ))} +
+
+ ) + }, + [isItemSelected, openItems, handleItemClick, toggleItemOpen, suitableCheck, toCodeName, toSuitableDetail], + ) + + // 메모이제이션된 아이템 리스트 + const renderedItems = useMemo(() => { + return visibleItems.map(renderItem) + }, [visibleItems, renderItem]) + + if (isSearchLoading) { + return
Loading...
+ } + + if (!suitableSearchResults?.suitable.length) { + return + } + + return ( + <> + {renderedItems} +
+ {isLoadingMore &&
데이터를 불러오는 중...
} +
+ + + ) +} diff --git a/src/components/suitable/SuitableListRaw.tsx b/src/components/suitable/SuitableListRaw.tsx new file mode 100644 index 0000000..6dc7f36 --- /dev/null +++ b/src/components/suitable/SuitableListRaw.tsx @@ -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>(new Set()) + const [visibleItems, setVisibleItems] = useState([]) + const [page, setPage] = useState(1) + const [isLoadingMore, setIsLoadingMore] = useState(false) + const observerTarget = useRef(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 ( +
+ +
+ ) + } else if (value === 'ー') { + return ( +
+ +
+ ) + } else { + return ( +
+ +
+ ) + } + }, []) + + // 메모이제이션된 아이템 렌더링 + const renderItem = useCallback( + (item: Suitable) => { + const isSelected = isItemSelected(item.id) + const isOpen = openItems.has(item.id) + + return ( +
+
+
+ handleItemClick(item.id)} /> + +
+
+ +
+
+
    + {toSuitableDetail(item.detail).map((subItem: SuitableDetail) => ( +
  • +
    +
    + + +
    +
    + {suitableCheck(subItem.trestleManufacturerProductName)} + {subItem.memo && ( +
    + +
    + )} +
    +
    +
  • + ))} +
+
+ ) + }, + [isItemSelected, openItems, handleItemClick, toggleItemOpen, suitableCheck, toCodeName, toSuitableDetail], + ) + + // 메모이제이션된 아이템 리스트 + const renderedItems = useMemo(() => { + return visibleItems.map(renderItem) + }, [visibleItems, renderItem]) + + if (isSearchLoading) { + return
Loading...
+ } + + if (!suitableSearchResults?.length) { + return + } + + return ( + <> + {renderedItems} +
+ {isLoadingMore &&
데이터를 불러오는 중...
} +
+ + + ) +} diff --git a/src/components/suitable/SuitableRaw.tsx b/src/components/suitable/SuitableRaw.tsx new file mode 100644 index 0000000..d48dfea --- /dev/null +++ b/src/components/suitable/SuitableRaw.tsx @@ -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 ( +
+ 테스트1 페이지 +
+ +
+
+
+ setSearchValue(e.target.value)} + /> + {searchValue &&
+
+
+
+
+
凡例
+
+ +
+
+
    +
  • +
    +
    + +
    + 設置可能 +
    +
  • +
  • +
    +
    + +
    + 設置不可 +
    +
  • +
  • +
    +
    + +
    + お問い合わせ +
    +
  • +
  • +
    +
    + +
    + 備考 +
    +
  • +
+
+ +
+
+ ) +} diff --git a/src/hooks/useCommCode.ts b/src/hooks/useCommCode.ts new file mode 100644 index 0000000..bb50240 --- /dev/null +++ b/src/hooks/useCommCode.ts @@ -0,0 +1,18 @@ +import { axiosInstance } from '@/libs/axios' +import type { CommCode } from '@/types/CommCode' + +export function useCommCode() { + const getCommCode = async (headCode: string): Promise => { + try { + const response = await axiosInstance(null).get('/api/comm-code', { params: { headCode: headCode } }) + return response.data + } catch (error) { + console.error(`common code (${headCode}) load failed:`, error) + return [] + } + } + + return { + getCommCode, + } +} diff --git a/src/hooks/useSuitable.ts b/src/hooks/useSuitable.ts index c3f0dde..4bdd9b2 100644 --- a/src/hooks/useSuitable.ts +++ b/src/hooks/useSuitable.ts @@ -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 => { try { - // return await suitableApi.getCategory() + const response = await axiosInstance(null).get('/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 => { + // try { + // const response = await axiosInstance(null).get('/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({ + 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({ + 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, + } } diff --git a/src/hooks/useSuitableRaw.ts b/src/hooks/useSuitableRaw.ts new file mode 100644 index 0000000..a962244 --- /dev/null +++ b/src/hooks/useSuitableRaw.ts @@ -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 => { + try { + const response = await axiosInstance(null).get('/api/suitable/list/test') + return response.data + } catch (error) { + console.error('지붕재 데이터 로드 실패:', error) + return [] + } + } + + // const updateSearchResults = async (selectedCategory: string | undefined, searchValue: string | undefined): Promise => { + // try { + // const response = await axiosInstance(null).get('/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({ + 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({ + } = useQuery({ + 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, + } +} diff --git a/src/libs/axios.ts b/src/libs/axios.ts index 6b5fbbc..a5d355c 100644 --- a/src/libs/axios.ts +++ b/src/libs/axios.ts @@ -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) } diff --git a/src/libs/partner.tsx b/src/libs/partner.tsx new file mode 100644 index 0000000..2b17677 --- /dev/null +++ b/src/libs/partner.tsx @@ -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 diff --git a/src/providers/EdgeProvider.tsx b/src/providers/EdgeProvider.tsx index 4cfd67e..de3c00f 100644 --- a/src/providers/EdgeProvider.tsx +++ b/src/providers/EdgeProvider.tsx @@ -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} diff --git a/src/store/useSuitableStore.ts b/src/store/useSuitableStore.ts index 17b88c5..5fa4cd0 100644 --- a/src/store/useSuitableStore.ts +++ b/src/store/useSuitableStore.ts @@ -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 - // // 검색 결과 설정 - // setSearchResults: (results: Suitable[]) => void - // // 검색 결과 초기화 - // resetSearchResults: () => void + /* 공통코드 */ + suitableCommCode: Map + /* 공통코드 설정 */ + 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((set) => ({ - // // 초기 상태 - // searchResults: [], + suitableCommCode: new Map() as Map, + 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 [] -// } -// } diff --git a/src/types/CommCode.ts b/src/types/CommCode.ts new file mode 100644 index 0000000..5847047 --- /dev/null +++ b/src/types/CommCode.ts @@ -0,0 +1,5 @@ +export type CommCode = { + headCd: string + code: string + codeJp: string +} diff --git a/src/types/Suitable.ts b/src/types/Suitable.ts new file mode 100644 index 0000000..2e3563b --- /dev/null +++ b/src/types/Suitable.ts @@ -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[] +} \ No newline at end of file