diff --git a/src/app/api/suitable/list/route.ts b/src/app/api/suitable/list/route.ts index 6f9260c..c668153 100644 --- a/src/app/api/suitable/list/route.ts +++ b/src/app/api/suitable/list/route.ts @@ -2,19 +2,58 @@ import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/libs/prisma' import { type Suitable } from '@/types/Suitable' +/** + * @api {get} /api/suitable/list 지붕재 적합성 데이터 조회 API + * @apiName GetSuitableList + * @apiGroup Suitable + * + * @apiDescription + * 지붕재 적합성 데이터의 일부 컬럼을 조회 + * + * @apiParam {Number} pageNumber 페이지 번호 + * @apiParam {Number} itemPerPage 페이지당 아이템 수 + * @apiParam {String} [category] 지붕재그룹코드 (예: RMG001) + * @apiParam {String} [keyword] 검색키워드 + * + * @apiExample {curl} Example usage: + * curl -X GET \ + * -G "pageNumber=1" \ + * -G "itemPerPage=10" \ + * -G "category=RMG001" \ + * -G "keyword=검색키워드" \ + * http://localhost:3000/api/suitable/list + * + * @apiSuccess {Array} suitable 지붕재 적합성 데이터 + * @apiSuccessExample {json} Success-Response: + * [ + * { + * "id": 1, + * "product_name": "test", + * "detail_cnt": 1, + * "detail": [ + * { + * "id": 1, + * "trestle_mfpc_cd": "test", + * "trestle_manufacturer_product_name": "test", + * "memo": "test" + * } + * ] + * } + * ] + */ export async function GET(request: NextRequest) { try { const searchParams = request.nextUrl.searchParams - const pageNumber = parseInt(searchParams.get('pageNumber') || '0') const itemPerPage = parseInt(searchParams.get('itemPerPage') || '0') + const category = searchParams.get('category') + const keyword = searchParams.get('keyword') + + /* 파라미터 체크 */ if (pageNumber === 0 || itemPerPage === 0) { return NextResponse.json({ error: '페이지 번호와 페이지당 아이템 수가 필요합니다' }, { status: 400 }) } - const category = searchParams.get('category') - const keyword = searchParams.get('keyword') - let query = ` SELECT msm.id @@ -48,7 +87,7 @@ export async function GET(request: NextRequest) { FETCH NEXT @P2 ROWS ONLY; ` - // 검색 조건 설정 + /* 검색 조건 설정 */ if (category) { const roofMtQuery = ` SELECT roof_mt_cd @@ -71,7 +110,7 @@ export async function GET(request: NextRequest) { }, }) } catch (error) { - console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) - return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) + console.error(`데이터 조회 중 오류가 발생했습니다: ${error}`) + return NextResponse.json({ error: `데이터 조회 중 오류가 발생했습니다: ${error}` }, { status: 500 }) } } diff --git a/src/app/api/suitable/pdf/route.ts b/src/app/api/suitable/pdf/route.ts index 1525cfb..768f81e 100644 --- a/src/app/api/suitable/pdf/route.ts +++ b/src/app/api/suitable/pdf/route.ts @@ -6,13 +6,35 @@ import { prisma } from '@/libs/prisma' import { type Suitable } from '@/types/Suitable' import SuitablePdf from '@/components/pdf/SuitablePdf' +/** + * @api {post} /api/suitable/pdf 지붕재 적합성 PDF 다운로드 API + * @apiName CreateSuitablePdf + * @apiGroup Suitable + * + * @apiDescription + * 지붕재 적합성 데이터를 PDF 파일로 다운로드 + * + * @apiBody {FormData} form-data + * @apiBody {String} form-data.ids 메인 ID 목록 (쉼표로 구분된 문자열) + * @apiBody {String} form-data.detailIds 상세 ID 목록 (쉼표로 구분된 문자열) + * @apiBody {String} form-data.fileTitle PDF 파일 제목 + * + * @apiExample {curl} Example usage: + * curl -X POST \ + * -F "ids=1,2,3" \ + * -F "detailIds=4,5,6" \ + * -F "fileTitle=적합성조사보고서" \ + * http://localhost:3000/api/suitable/pdf + * + * @apiSuccess {File} 지붕재 적합성 PDF 파일 + */ export async function POST(request: NextRequest) { - // 파라미터 체크 const formData = await request.formData() const ids = formData.get('ids') as string const detailIds = formData.get('detailIds') as string const fileTitle = formData.get('fileTitle') as string + /* 파라미터 체크 */ if (ids === '' || detailIds === '' || fileTitle === '') { return NextResponse.json({ error: '필수 파라미터가 누락되었습니다' }, { status: 400 }) } @@ -59,14 +81,14 @@ export async function POST(request: NextRequest) { ORDER BY msm.product_name; ` - // 검색 조건 설정 + /* 검색 조건 설정 */ query = query.replaceAll(':mainIds', ids) query = query.replaceAll(':detailIds', detailIds) - // 데이터 조회 + /* 데이터 조회 */ const suitable: Suitable[] = await prisma.$queryRawUnsafe(query) - // pdf 생성 : mainId 100개씩 청크로 나누기 + /* pdf 생성 : mainId 100개씩 청크로 나누기 */ const CHUNK_SIZE = 100 const pdfBuffers: Uint8Array[] = [] for (let i = 0; i < suitable.length; i += CHUNK_SIZE) { @@ -85,7 +107,7 @@ export async function POST(request: NextRequest) { pdfBuffers.push(new Uint8Array(arrayBuffer)) } - // 모든 PDF 버퍼 병합 + /* 모든 PDF 버퍼 병합 */ const mergedPdfBytes = await mergePdfBuffers(pdfBuffers) const fileName = `${fileTitle.replace(' ', '_')}.pdf` @@ -98,11 +120,16 @@ export async function POST(request: NextRequest) { }, }) } catch (error) { - console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) - return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) + console.error(`데이터 조회 중 오류가 발생했습니다: ${error}`) + return NextResponse.json({ error: `데이터 조회 중 오류가 발생했습니다: ${error}` }, { status: 500 }) } } +/** + * @description PDF 버퍼 병합 + * @param buffers 병합할 PDF 버퍼 + * @returns 병합된 PDF 버퍼 + */ async function mergePdfBuffers(buffers: Uint8Array[]) { const mergedPdf = await PDFDocument.create() diff --git a/src/app/api/suitable/pick/route.ts b/src/app/api/suitable/pick/route.ts index 922a1d3..2865777 100644 --- a/src/app/api/suitable/pick/route.ts +++ b/src/app/api/suitable/pick/route.ts @@ -1,6 +1,32 @@ import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/libs/prisma' +/** + * @api {get} /api/suitable/pick 지붕재 적합성 데이터 아이디 조회 API + * @apiName GetSuitablePick + * @apiGroup Suitable + * + * @apiDescription + * 지붕재 적합성 데이터의 검색 조건에 맞는 main_id와 detail_id를 조회 + * + * @apiParam {String} [category] 지붕재그룹코드 (예: RMG001) + * @apiParam {String} [keyword] 검색키워드 + * + * @apiExample {curl} Example usage: + * curl -X GET \ + * -G "category=RMG001" \ + * -G "keyword=검색키워드" \ + * http://localhost:3000/api/suitable/pick + * + * @apiSuccess {Array} suitableIdSet 지붕재 적합성 데이터의 아이디 목록 + * @apiSuccessExample {json} Success-Response: + * [ + * { + * "id": 1, + * "detail_id": "1,2,3" + * } + * ] + */ export async function GET(request: NextRequest) { try { const searchParams = request.nextUrl.searchParams @@ -26,7 +52,7 @@ export async function GET(request: NextRequest) { ; ` - // 검색 조건 설정 + /* 검색 조건 설정 */ if (category) { const roofMtQuery = ` SELECT roof_mt_cd @@ -45,7 +71,7 @@ export async function GET(request: NextRequest) { return NextResponse.json(suitableIdSet) } catch (error) { - console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) - return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) + console.error(`데이터 조회 중 오류가 발생했습니다: ${error}`) + return NextResponse.json({ error: `데이터 조회 중 오류가 발생했습니다: ${error}` }, { status: 500 }) } } diff --git a/src/app/api/suitable/route.ts b/src/app/api/suitable/route.ts index 173b6ea..888e663 100644 --- a/src/app/api/suitable/route.ts +++ b/src/app/api/suitable/route.ts @@ -2,12 +2,51 @@ import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/libs/prisma' import { Suitable } from '@/types/Suitable' +/** + * @api {post} /api/suitable 지붕재 적합성 데이터 모든 컬럼 조회 API + * @apiName GetSuitable + * @apiGroup Suitable + * + * @apiDescription + * 지붕재 적합성 데이터의 모든 컬럼을 조회 + * + * @apiBody {FormData} form-data + * @apiBody {String} form-data.ids 메인 ID 목록 (쉼표로 구분된 문자열) + * @apiBody {String} form-data.detailIds 상세 ID 목록 (쉼표로 구분된 문자열) + * + * @apiExample {curl} Example usage: + * curl -X POST \ + * -F "ids=1,2,3" \ + * -F "detailIds=4,5,6" \ + * http://localhost:3000/api/suitable + * + * @apiSuccess {Array} suitable 지붕재 적합성 데이터 + * @apiSuccessExample {json} Success-Response: + * [ + * { + * "id": 1, + * "product_name": "test", + * "manu_ft_cd": "test", + * "roof_mt_cd": "test", + * "roof_sh_cd": "test", + * "detail": [ + * { + * "id": 1, + * "trestle_mfpc_cd": "MFPC001", + * "trestle_manufacturer_product_name": "test", + * "memo": "test" + * } + * ] + * } + * ] + */ export async function POST(request: NextRequest) { try { const body: Record = await request.json() const ids = body.ids const detailIds = body.detailIds + /* 파라미터 체크 */ if (ids === '' || detailIds === '') { return NextResponse.json({ error: '필수 파라미터가 누락되었습니다' }, { status: 400 }) } @@ -45,7 +84,7 @@ export async function POST(request: NextRequest) { ORDER BY msm.product_name; ` - // 검색 조건 설정 + /* 검색 조건 설정 */ query = query.replaceAll(':mainIds', ids) query = query.replaceAll(':detailIds', detailIds) @@ -53,7 +92,7 @@ export async function POST(request: NextRequest) { return NextResponse.json(suitable) } catch (error) { - console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) - return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) + console.error(`데이터 조회 중 오류가 발생했습니다: ${error}`) + return NextResponse.json({ error: `데이터 조회 중 오류가 발생했습니다: ${error}` }, { status: 500 }) } } diff --git a/src/components/popup/SuitableDetailPopupButton.tsx b/src/components/popup/SuitableDetailPopupButton.tsx index 8340f94..06bb8d0 100644 --- a/src/components/popup/SuitableDetailPopupButton.tsx +++ b/src/components/popup/SuitableDetailPopupButton.tsx @@ -9,10 +9,12 @@ export default function SuitableDetailPopupButton() { const popupController = usePopupController() const { downloadSuitablePdf } = useSuitable() + /* 상세 팝업 닫기 */ const handleClosePopup = () => { popupController.setSuitableDetailPopup(false) } + /* 페이지 이동 */ const handleRedirectPage = (path: string) => { handleClosePopup() router.push(path) diff --git a/src/components/suitable/SuitableButton.tsx b/src/components/suitable/SuitableButton.tsx index b651a26..3ce8a3c 100644 --- a/src/components/suitable/SuitableButton.tsx +++ b/src/components/suitable/SuitableButton.tsx @@ -9,10 +9,12 @@ export default function SuitableButton() { const { getSuitableIds, clearSuitableStore, downloadSuitablePdf } = useSuitable() const { selectedItems, addAllSelectedItem } = useSuitableStore() + /* 데이터 전체 선택 */ const handleSelectAll = async () => { addAllSelectedItem(await getSuitableIds()) } + /* 상세 팝업 열기 */ const handleOpenPopup = () => { if (selectedItems.size === 0) { alert('屋根材を選択してください。') @@ -21,6 +23,7 @@ export default function SuitableButton() { popupController.setSuitableDetailPopup(true) } + /* pdf 다운로드 */ const handleRedirectPdfDownload = () => { if (selectedItems.size === 0) { alert('屋根材を選択してください。') diff --git a/src/hooks/useSuitable.ts b/src/hooks/useSuitable.ts index 0bbc29b..8ee7ca7 100644 --- a/src/hooks/useSuitable.ts +++ b/src/hooks/useSuitable.ts @@ -20,6 +20,16 @@ export function useSuitable() { clearSelectedItems, } = useSuitableStore() + /** + * @description 지붕재 적합성 데이터 조회. API를 호출하여 검색 조건에 맞는 지붕재 적합성 데이터를 페이지 단위로 조회 + * + * @param {Object} param + * @param {number} [param.pageNumber] 페이지 번호 (기본값: 1) + * @param {string} [param.category] 지붕재그룹코드 + * @param {string} [param.keyword] 검색키워드 + * + * @returns {Promise} 지붕재 적합성 데이터 + */ const getSuitables = async ({ pageNumber, category, @@ -40,11 +50,16 @@ export function useSuitable() { const response = await axiosInstance(null).get('/api/suitable/list', { params }) return response.data } catch (error) { - console.error('지붕재 데이터 로드 실패:', error) + console.error(`지붕재 적합성 데이터 조회 실패: ${error}`) return [] } } + /** + * @description 지붕재 적합성 데이터 아이디 조회. API를 호출하여 검색 조건에 맞는 지붕재 적합성 데이터의 main_id, detail_id를 조회 + * + * @returns {Promise} 지붕재 적합성 데이터 아이디 목록 + */ const getSuitableIds = async (): Promise => { try { const params: Record = {} @@ -53,11 +68,19 @@ export function useSuitable() { const response = await axiosInstance(null).get('/api/suitable/pick', { params }) return response.data } catch (error) { - console.error('지붕재 아이디 로드 실패:', error) + console.error(`지붕재 적합성 데이터 아이디 조회 실패: ${error}`) return [] } } + /** + * @description 지붕재 적합성 데이터 조회. API를 호출하여 지붕재 적합성 데이터의 모든 컬럼을 조회 + * + * @param {string} ids 지붕재 적합성 데이터의 main_id (쉼표로 구분된 문자열) + * @param {string} [detailIds] 지붕재 적합성 데이터의 detail_id (쉼표로 구분된 문자열) + * + * @returns {Promise} 지붕재 적합성 데이터 + */ const getSuitableDetails = async (ids: string, detailIds?: string): Promise => { try { const params: Record = { ids: ids } @@ -65,12 +88,17 @@ export function useSuitable() { const response = await axiosInstance(null).post('/api/suitable', params) return response.data } catch (error) { - console.error('지붕재 상세 데이터 로드 실패:', error) + console.error(`지붕재 적합성 상세 데이터 조회 실패: ${error}`) return [] } } - const getSuitableCommCode = () => { + /** + * @description 지붕재 적합성 공통 코드 조회. API를 호출하여 지붕재 적합성 데이터에서 사용하는 공통 코드 조회 후 스토어에 맵핑하여 저장 + * + * @returns {void} + */ + const getSuitableCommCode = (): void => { const headCodes = Object.values(SUITABLE_HEAD_CODE) as SUITABLE_HEAD_CODE[] for (const code of headCodes) { getCommCode(code).then((res) => { @@ -79,11 +107,26 @@ export function useSuitable() { } } + /** + * @description 지붕재 적합성 공통 코드의 JP 코드명 조회 + * + * @param {string} headCode 공통 코드의 head_code + * @param {string} code 공통 코드의 code + * + * @returns {string} 공통 코드의 JP 코드명 + */ const toCodeName = (headCode: string, code: string): string => { const commCode = suitableCommCode.get(headCode) return commCode?.find((item) => item.code === code)?.codeJp || '' } + /** + * @description 지붕재 적합성 detail 데이터를 string 형태로 받아 파싱하여 반환 + * + * @param {string} suitableDetailString 지붕재 적합성 detail string + * + * @returns {SuitableDetail[]} 파싱된 지붕재 적합성 detail 데이터 + */ const toSuitableDetail = (suitableDetailString: string): SuitableDetail[] => { if (!suitableDetailString) return [] try { @@ -93,20 +136,32 @@ export function useSuitable() { } return suitableDetailArray } catch (error) { - console.error('지붕재 데이터 파싱 실패:', error) + console.error(`지붕재 적합성 detail 데이터 파싱 실패: ${error}`) return [] } } + /** + * @description 지붕재 적합성 상위 데이터 클릭 시 호출되는 함수. 지붕재 적합성 detail 데이터에서 id 목록을 추출하여 Set 형태로 반환 + * + * @param {string} suitableDetailString 지붕재 적합성 detail 데이터 + * + * @returns {Set} 지붕재 적합성 detail 데이터의 id 목록 + */ const toSuitableDetailIds = (suitableDetailString: string): Set => { try { return new Set(JSON.parse(suitableDetailString).map(({ id }: { id: number }) => id)) } catch (error) { - console.error('지붕재 데이터 파싱 실패:', error) + console.error(`지붕재 적합성 detail 데이터 파싱 실패: ${error}`) return new Set() } } + /** + * @description 지붕재 적합성 조회 페이지에서 보여지는 지붕재 적합성 데이터 조회 + * + * @returns {Object} 지붕재 적합성 데이터 조회 결과 + */ const { data: suitables, fetchNextPage, @@ -135,12 +190,29 @@ export function useSuitable() { enabled: searchCategory !== '' || searchKeyword !== '', }) + /** + * @description 지붕재 적합성 스토어 초기화. 선택된 지붕재 적합성 데이터, 검색 카테고리, 검색 키워드 초기화 + * + * @param {Object} param + * @param {boolean} [param.items] 선택된 지붕재 적합성 데이터 초기화 여부 + * @param {boolean} [param.category] 검색 카테고리 초기화 여부 + * @param {boolean} [param.keyword] 검색 키워드 초기화 여부 + * + * @returns {void} + */ const clearSuitableStore = ({ items = false, category = false, keyword = false }: { items?: boolean; category?: boolean; keyword?: boolean }) => { if (items) clearSelectedItems() if (category) clearSearchCategory() if (keyword) clearSearchKeyword() } + /** + * @description 지붕재 적합성 비고에 따른 아이콘 반환 + * + * @param {string} value 지붕재 적합성 비고 + * + * @returns {string} 지붕재 적합성 비고에 따른 아이콘 + */ // TODO: 추후 지붕재 적합성 데이터 CUD 구현 시 ×, -, ー 데이터 관리 필요 const suitableCheckIcon = (value: string): string => { const iconMap: Record = { @@ -152,6 +224,13 @@ export function useSuitable() { return iconMap[value] || iconMap.default } + /** + * @description 지붕재 적합성 비고에 따른 적합 결과 반환 + * + * @param {string} value 지붕재 적합성 비고 + * + * @returns {string} 지붕재 적합성 비고에 따른 적합 결과 + */ // TODO: 추후 지붕재 적합성 데이터 CUD 구현 시 ○, ×, -, ー 데이터 관리 필요 const suitableCheckMemo = (value: string): string => { if (value === '○') return '設置可' @@ -160,6 +239,13 @@ export function useSuitable() { return `${value}で設置可` } + /** + * @description 선택된 지붕재 적합성 데이터의 main_id, detail_id를 쉼표로 구분하여 반환. 선택된 지붕재 적합성 데이터가 없으면 빈 문자열 반환 + * + * @returns {Object} 선택된 지붕재 적합성 데이터의 main_id, detail_id + * @returns {string} 선택된 지붕재 적합성 데이터의 main_id (쉼표로 구분된 문자열) + * @returns {string} 선택된 지붕재 적합성 데이터의 detail_id (쉼표로 구분된 문자열) + */ const serializeSelectedItems = (): { ids: string; detailIds: string } => { const ids: string[] = [] const detailIds: string[] = [] @@ -170,11 +256,21 @@ export function useSuitable() { return { ids: ids.join(','), detailIds: detailIds.length > 0 ? detailIds.join(',') : '' } } + /** + * @description 선택된 지붕재 적합성 데이터 조회. API를 호출하여 선택된 지붕재 적합성 데이터의 모든 컬럼을 조회 + * + * @returns {Promise} 선택된 지붕재 적합성 데이터 + */ const getSelectedSuitables = async (): Promise => { const { ids, detailIds } = serializeSelectedItems() return await getSuitableDetails(ids, detailIds) } + /** + * @description 선택된 지붕재 적합성 데이터 pdf 다운로드. form 태그를 생성하여 직접 API를 호출하여 pdf 다운로드 + * + * @returns {Promise} 선택된 지붕재 적합성 데이터 pdf 다운로드 + */ const downloadSuitablePdf = async (): Promise => { try { const { ids, detailIds } = serializeSelectedItems() @@ -210,7 +306,7 @@ export function useSuitable() { form.submit() document.body.removeChild(form) } catch (error) { - console.error('지붕재 상세 데이터 pdf 다운로드 실패:', error) + console.error(`지붕재 적합성 상세 데이터 pdf 다운로드 실패: ${error}`) } }