From e3940f72c88e221eeaeba3520031d90beabbca95 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Tue, 13 May 2025 09:22:35 +0900 Subject: [PATCH 01/19] feat: implement Qsp Inquriy API Request, Response type --- src/app/api/qna/detail/route.ts | 15 ++ src/app/api/qna/list/route.ts | 17 ++ src/app/api/qna/save/route.ts | 15 ++ src/app/inquiry/list/page.tsx | 4 +- src/components/inquiry/Answer.tsx | 28 ++- src/components/inquiry/Detail.tsx | 48 +++-- src/components/inquiry/InquiryDetail.tsx | 73 -------- src/components/inquiry/InquiryFilter.tsx | 20 -- src/components/inquiry/InquiryItems.tsx | 21 --- src/components/inquiry/InquiryList.tsx | 171 ------------------ src/components/inquiry/InquiryWriteForm.tsx | 67 ------- src/components/inquiry/ListTable.tsx | 93 ---------- .../inquiry/{ => list}/ListForm.tsx | 0 src/components/inquiry/list/ListTable.tsx | 76 ++++++++ src/hooks/useInquiry.ts | 60 ++++++ src/store/inquiryFilterStore.ts | 66 +++---- src/types/Inquiry.ts | 89 +++++++++ 17 files changed, 340 insertions(+), 523 deletions(-) create mode 100644 src/app/api/qna/detail/route.ts create mode 100644 src/app/api/qna/list/route.ts create mode 100644 src/app/api/qna/save/route.ts delete mode 100644 src/components/inquiry/InquiryDetail.tsx delete mode 100644 src/components/inquiry/InquiryFilter.tsx delete mode 100644 src/components/inquiry/InquiryItems.tsx delete mode 100644 src/components/inquiry/InquiryList.tsx delete mode 100644 src/components/inquiry/InquiryWriteForm.tsx delete mode 100644 src/components/inquiry/ListTable.tsx rename src/components/inquiry/{ => list}/ListForm.tsx (100%) create mode 100644 src/components/inquiry/list/ListTable.tsx create mode 100644 src/hooks/useInquiry.ts create mode 100644 src/types/Inquiry.ts diff --git a/src/app/api/qna/detail/route.ts b/src/app/api/qna/detail/route.ts new file mode 100644 index 0000000..a9b670e --- /dev/null +++ b/src/app/api/qna/detail/route.ts @@ -0,0 +1,15 @@ +import { queryStringFormatter } from '@/utils/common-utils' +import axios from 'axios' +import { NextResponse } from 'next/server' + +export const QSP_URL = 'http://localhost:8080' +export async function GET(request: Request) { + const body = await request.json() + + const response = await axios.get(`${QSP_URL}/qna/detail?${queryStringFormatter(body)}`) + + if (response.status === 200) { + return NextResponse.json(response.data) + } + return NextResponse.json({ error: 'Failed to fetch qna detail' }, { status: response.status }) +} diff --git a/src/app/api/qna/list/route.ts b/src/app/api/qna/list/route.ts new file mode 100644 index 0000000..9e0f651 --- /dev/null +++ b/src/app/api/qna/list/route.ts @@ -0,0 +1,17 @@ +import axios from 'axios' +import { NextResponse } from 'next/server' +import { queryStringFormatter } from '@/utils/common-utils' +import { QSP_URL } from '../detail/route' + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url) + console.log('searchParams::: ', searchParams) + + const response = await axios.get(`${QSP_URL}/qna/list?${queryStringFormatter(searchParams)}`) + + if (response.status === 200) { + return NextResponse.json(response.data) + } + + return NextResponse.json({ error: 'Failed to fetch qna list' }, { status: response.status }) +} diff --git a/src/app/api/qna/save/route.ts b/src/app/api/qna/save/route.ts new file mode 100644 index 0000000..8f29873 --- /dev/null +++ b/src/app/api/qna/save/route.ts @@ -0,0 +1,15 @@ +import axios from 'axios' +import { NextResponse } from 'next/server' +import { QSP_URL } from '../detail/route' + +export async function POST(request: Request) { + const body = await request.json() + + const response = await axios.post(`${QSP_URL}/qna/save`, body) + + if (response.status === 200) { + return NextResponse.json(response.data) + } + + return NextResponse.json({ error: 'Failed to save qna' }, { status: response.status }) +} diff --git a/src/app/inquiry/list/page.tsx b/src/app/inquiry/list/page.tsx index 93fb334..7e7654e 100644 --- a/src/app/inquiry/list/page.tsx +++ b/src/app/inquiry/list/page.tsx @@ -1,5 +1,5 @@ -import ListForm from '@/components/inquiry/ListForm' -import ListTable from '@/components/inquiry/ListTable' +import ListForm from '@/components/inquiry/list/ListForm' +import ListTable from '@/components/inquiry/list/ListTable' export default function page() { return ( diff --git a/src/components/inquiry/Answer.tsx b/src/components/inquiry/Answer.tsx index c1a5ec9..5628f36 100644 --- a/src/components/inquiry/Answer.tsx +++ b/src/components/inquiry/Answer.tsx @@ -1,35 +1,31 @@ 'use client' -export default function Answer() { +import { Inquiry } from '@/types/Inquiry' + +export default function Answer({ inquiryDetail }: { inquiryDetail: Inquiry }) { return ( <>
Hanwha Japan 回答
- 佐藤一貴/ 2025.04.02 16:54:00 + {inquiryDetail?.ansRegNm}/ {inquiryDetail?.ansRegDt}
回答
-
- 一次側接続は, 自動切替開閉器と住宅分電盤昼間遮断器との間に蓄電システム遮断器を配線する方法です. 二次側接続は, - 住宅分電盤週間ブレーカの二次側に蓄電システムブレーカを接続する -
+
{inquiryDetail?.ansContents}
ファイル添付
    -
  • - -
  • -
  • - -
  • + {inquiryDetail?.ansListFile?.map((file) => ( +
  • + +
  • + ))}
diff --git a/src/components/inquiry/Detail.tsx b/src/components/inquiry/Detail.tsx index d41d5a0..51fe7d3 100644 --- a/src/components/inquiry/Detail.tsx +++ b/src/components/inquiry/Detail.tsx @@ -2,19 +2,22 @@ import { useState } from 'react' import Answer from './Answer' +import { useInquiry } from '@/hooks/useInquiry' +import { useRouter } from 'next/navigation' export default function Detail() { //todo: 답변 완료 표시를 위해 임시로 추가 해 놓은 state // 추후에 api 작업 완료후 삭제 // 답변 완료 클래스 & 하단 답변내용 출력도 - const [inquiry, setInquiry] = useState(false) + const { inquiryDetail } = useInquiry() + const router = useRouter() return ( <>
-
回答完了
+
回答完了
@@ -25,59 +28,50 @@ export default function Detail() { - + - + - + - + - +
登録日2025.04.10{inquiryDetail?.regDt}
作者Hong gi{inquiryDetail?.regNm}
販売店interplug{inquiryDetail?.storeNm}
施工店interplugs{inquiryDetail?.compCd}
E-mailHong@interplug.co.kr{inquiryDetail?.regEmail}
屋根適合
-
屋根材適合性確認依頼
-
- 入力した内容が表示されます. -
- インストール可能であることを確認してください. -
- 屋根の写真を添付しました. -
+
{inquiryDetail?.qstTitle}
+
{inquiryDetail?.qstContent}
ファイル添付
    -
  • - -
  • -
  • - -
  • + {inquiryDetail?.listFile?.map((file) => ( +
  • + +
  • + ))}
- {inquiry && } + {inquiryDetail?.answerYn === 'Y' && }
-
diff --git a/src/components/inquiry/InquiryDetail.tsx b/src/components/inquiry/InquiryDetail.tsx deleted file mode 100644 index b153f35..0000000 --- a/src/components/inquiry/InquiryDetail.tsx +++ /dev/null @@ -1,73 +0,0 @@ -'use client' - -import { useParams } from 'next/navigation' - -const inquiryDummyData = { - writer: { - name: 'writer', - email: 'writer@example.com', - }, - title: 'title', - content: 'content', - files: ['file1.jpg', 'file2.jpg', 'file3.jpg'], - createdAt: '2021-01-01', - answer: { - writer: '佐藤一貴', - content: - '一次側接続は、自動切替開閉器と住宅分電盤主幹ブレーカの間に蓄電システムブレーカを配線する方法です。\n二次側接続は、住宅分電盤主幹ブレ―カの二次側に蓄電システムブレーカを接続する', - createdAt: '2021-01-01 12:00:00', - files: ['file4.jpg', 'file5.jpg', 'file6.jpg'], - }, -} - -export default function InquiryDetail() { - const params = useParams() - const id = params.id - return ( -
-

InquiryDetail

-

{id}

-
-
-

writer

-

{inquiryDummyData.writer.name}

-
-
-

email

-

{inquiryDummyData.writer.email}

-
-
-

title

-

{inquiryDummyData.title}

-
-
-

content

-

{inquiryDummyData.content}

-
-
-

files

-
- {inquiryDummyData.files.map((file) => ( - {file} - ))} -
-
- {inquiryDummyData.answer && ( -
-

Reply: Hanwha Japan

-
-

{inquiryDummyData.answer.writer}

-

{inquiryDummyData.answer.createdAt}

-

{inquiryDummyData.answer.content}

-
- {inquiryDummyData.answer.files.map((file) => ( - {file} - ))} -
-
-
- )} -
-
- ) -} diff --git a/src/components/inquiry/InquiryFilter.tsx b/src/components/inquiry/InquiryFilter.tsx deleted file mode 100644 index c3911a2..0000000 --- a/src/components/inquiry/InquiryFilter.tsx +++ /dev/null @@ -1,20 +0,0 @@ -'use client' - -import { Search } from 'lucide-react' -import { useRouter } from 'next/navigation' - - -export default function InquiryFilter({ handleSearch }: { handleSearch: (e: React.ChangeEvent) => void }) { - const router = useRouter() - return ( -
- -
- - -
-
- ) -} diff --git a/src/components/inquiry/InquiryItems.tsx b/src/components/inquiry/InquiryItems.tsx deleted file mode 100644 index bc38ad6..0000000 --- a/src/components/inquiry/InquiryItems.tsx +++ /dev/null @@ -1,21 +0,0 @@ -'use client' - -import { useRouter } from 'next/navigation' - -export default function InquiryItems({ inquiryData }: { inquiryData: any }) { - const router = useRouter() - return ( -
- {inquiryData.map((item: any) => ( -
router.push(`/inquiry/${item.id}`)}> -
{item.title}
-
{item.content}
-
{item.createdAt}
-
{item.writer}
-
{item.category}
- {item.file &&
{item.file}
} -
- ))} -
- ) -} diff --git a/src/components/inquiry/InquiryList.tsx b/src/components/inquiry/InquiryList.tsx deleted file mode 100644 index f65478b..0000000 --- a/src/components/inquiry/InquiryList.tsx +++ /dev/null @@ -1,171 +0,0 @@ -'use client' -import { useState } from 'react' -import InquiryItems from './InquiryItems' -import InquiryFilter from './InquiryFilter' -import LoadMoreButton from '../LoadMoreButton' - -const inquiryDummyData = [ - { - id: 1, - title: 'post', - content: 'content', - file: 'file.png', - createdAt: '2024-01-01', - writer: 'writer', - category: 'A', - }, - { - id: 2, - title: 'post', - content: 'content', - file: 'file.png', - createdAt: '2024-01-01', - writer: 'writer1', - category: 'B', - }, - { - id: 3, - title: 'post', - content: 'content', - file: null, - createdAt: '2024-01-01', - writer: 'writer1', - category: 'C', - }, - { - id: 4, - title: 'post', - content: 'content', - file: null, - createdAt: '2024-01-01', - writer: 'writer1', - category: 'A', - }, - { - id: 5, - title: 'post', - content: 'content', - file: null, - createdAt: '2024-01-01', - writer: 'writer1', - category: 'B', - }, - { - id: 6, - title: 'post', - content: 'content', - file: null, - createdAt: '2024-01-01', - writer: 'writer1', - category: 'C', - }, - { - id: 7, - title: 'post', - content: 'content', - file: 'file.png', - createdAt: '2024-01-01', - writer: 'writer', - category: 'A', - }, - { - id: 8, - title: 'post', - content: 'content', - file: 'file.png', - createdAt: '2024-01-01', - writer: 'writer1', - category: 'B', - }, - { - id: 9, - title: 'post', - content: 'content', - file: null, - createdAt: '2024-01-01', - writer: 'writer1', - category: 'C', - }, - - { - id: 10, - title: 'post', - content: 'content', - file: 'file.png', - createdAt: '2024-01-01', - writer: 'writer1', - category: 'A', - }, - { - id: 11, - title: 'post', - content: 'content', - file: 'file.png', - createdAt: '2024-01-01', - writer: 'writer', - category: 'B', - }, - { - id: 12, - title: 'post', - content: 'content', - file: null, - createdAt: '2024-01-01', - writer: 'writer1', - category: 'C', - }, -] - -export default function InquiryList() { - const [visibleItems, setVisibleItems] = useState(5) - const [isMyPostsOnly, setIsMyPostsOnly] = useState(false) - const [category, setCategory] = useState('') - const [search, setSearch] = useState('') - const [hasMore, setHasMore] = useState(inquiryDummyData.length > 5) - - const inquriyData = () => { - if (isMyPostsOnly) { - return inquiryDummyData.filter((item) => item.writer === 'writer') - } - if (category.trim().length > 0) { - return inquiryDummyData.filter((item) => item.category === category) - } - if (search.trim().length > 0) { - return inquiryDummyData.filter((item) => item.title.includes(search)) - } - return inquiryDummyData - } - - const handleLoadMore = () => { - const newVisibleItems = Math.min(visibleItems + 5, inquriyData().length) - setVisibleItems(newVisibleItems) - setHasMore(newVisibleItems < inquriyData().length) - } - - const handleSearch = (e: React.ChangeEvent) => { - setSearch(e.target.value) - } - - const handleScrollToTop = () => { - window.scrollTo({ top: 0, behavior: 'smooth' }) - } - - return ( -
- -
- setIsMyPostsOnly(e.target.checked)} /> - -
- - total {inquriyData().length} - - -
- ) -} diff --git a/src/components/inquiry/InquiryWriteForm.tsx b/src/components/inquiry/InquiryWriteForm.tsx deleted file mode 100644 index 868f3da..0000000 --- a/src/components/inquiry/InquiryWriteForm.tsx +++ /dev/null @@ -1,67 +0,0 @@ -'use client' - -import { useState } from 'react' -import { useRouter } from 'next/navigation' - -export interface InquiryFormData { - category: string - title: string - content: string - file: File[] -} - -export default function InquiryWriteForm() { - const router = useRouter() - const [formData, setFormData] = useState({ - category: 'A', - title: '', - content: '', - file: [], - }) - - const handleFileChange = (e: React.ChangeEvent) => { - const file = Array.from(e.target.files || []) - setFormData({ ...formData, file: [...formData.file, ...file] }) - } - const handleSubmit = () => { - console.log('submit: ', formData) - // router.push(`/inquiry`) - } - - return ( -
-
- - -
-
- - setFormData({ ...formData, title: e.target.value })} /> -
-
- - +
@@ -46,29 +122,25 @@ export default function RegistForm() { - +
- 添付ファイル2個 + 添付ファイル{attachedFiles.length}
    -
  • -
    -
    添付ファイル名.jpg
    - -
    -
  • -
  • -
    -
    添付ファイル名.jpg
    - -
    -
  • + {attachedFiles.map((file, index) => ( +
  • +
    +
    {file.name}
    +
    +
  • + ))}
-
diff --git a/src/components/inquiry/list/ListForm.tsx b/src/components/inquiry/list/ListForm.tsx index 14f38bb..d22d424 100644 --- a/src/components/inquiry/list/ListForm.tsx +++ b/src/components/inquiry/list/ListForm.tsx @@ -1,8 +1,25 @@ 'use client' +import { useInquiryFilterStore } from '@/store/inquiryFilterStore' import { useRouter } from 'next/navigation' +import { useState } from 'react' export default function ListForm() { const router = useRouter() + const [searchKeyword, setSearchKeyword] = useState('') + const { inquiryListRequest, setInquiryListRequest } = useInquiryFilterStore() + + const handleSearch = () => { + if (searchKeyword.length >= 2) { + setInquiryListRequest({ ...inquiryListRequest, schTitle: searchKeyword }) + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleSearch() + } + } + return ( <>
@@ -13,8 +30,15 @@ export default function ListForm() {
- - + setSearchKeyword(e.target.value)} + onKeyDown={handleKeyDown} + /> +
diff --git a/src/components/inquiry/list/ListTable.tsx b/src/components/inquiry/list/ListTable.tsx index 16314aa..9598117 100644 --- a/src/components/inquiry/list/ListTable.tsx +++ b/src/components/inquiry/list/ListTable.tsx @@ -4,6 +4,9 @@ import { useEffect, useState } from 'react' import LoadMoreButton from '../../LoadMoreButton' import { useInquiry } from '@/hooks/useInquiry' import { InquiryList } from '@/types/Inquiry' +import { useRouter } from 'next/navigation' +import { useInquiryFilterStore } from '@/store/inquiryFilterStore' +import { useSessionStore } from '@/store/session' const badgeStyle = [ { @@ -21,14 +24,30 @@ export default function ListTable() { const [offset, setOffset] = useState(0) const [hasMore, setHasMore] = useState(true) + const router = useRouter() + const { inquiryList } = useInquiry() + const { inquiryListRequest, setInquiryListRequest } = useInquiryFilterStore() + + const { session } = useSessionStore() + useEffect(() => { - if (inquiryList.length > offset + 10) { - setHasMore(true) + if (inquiryList.length !== 0) { + const hasMoreItems = inquiryList[0].totCnt > offset + 10 + setHasMore(hasMoreItems) } else { setHasMore(false) } - }, [inquiryList]) + }, [inquiryList, offset]) + + const handleMyInquiry = () => { + setInquiryListRequest({ ...inquiryListRequest, schRegId: inquiryListRequest.schRegId ? null : session.userId }) + } + + const handleLoadMore = () => { + setOffset(offset + 10) + setInquiryListRequest({ ...inquiryListRequest, startRow: offset, endRow: offset + 10 }) + } return ( <> @@ -36,7 +55,7 @@ export default function ListTable() {
- +
@@ -53,21 +72,22 @@ export default function ListTable() { 合計 {inquiryList.length}
    - {inquiryList.map((inquiry: InquiryList) => ( -
  • -
    -
    {inquiry.qnaTpCd}
    -
    {inquiry.qstTitle}
    -
    {inquiry.regDt}
    -
    badge.id === inquiry.answerYn)?.color}`}> - {badgeStyle.find((badge) => badge.id === inquiry.answerYn)?.label} + {inquiryList.length > 0 && + inquiryList.map((inquiry: InquiryList) => ( +
  • router.push(`/inquiry/${inquiry.qnaNo}`)}> +
    +
    {inquiry.qnaClsLrgCd}
    +
    {inquiry.qstTitle}
    +
    {inquiry.regDt}
    +
    badge.id === inquiry.answerYn)?.color}`}> + {badgeStyle.find((badge) => badge.id === inquiry.answerYn)?.label} +
    - -
  • - ))} + + ))}
- setOffset(offset + 10)} /> + handleLoadMore()} />
diff --git a/src/hooks/useInquiry.ts b/src/hooks/useInquiry.ts index 07e74fe..17c3099 100644 --- a/src/hooks/useInquiry.ts +++ b/src/hooks/useInquiry.ts @@ -1,4 +1,4 @@ -import { InquiryList, Inquiry, InquiryRequest } from '@/types/Inquiry' +import { InquiryList, Inquiry, InquiryRequest, InquirySaveResponse } from '@/types/Inquiry' import { axiosInstance } from '@/libs/axios' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useInquiryFilterStore } from '@/store/inquiryFilterStore' @@ -12,37 +12,47 @@ export function useInquiry( inquiryDetail: Inquiry | null isLoadingInquiryDetail: boolean isSavingInquiry: boolean - saveInquiry: (inquiryRequest: InquiryRequest) => Promise + saveInquiry: (inquiryRequest: InquiryRequest) => Promise } { const { session } = useSessionStore() const queryClient = useQueryClient() const { inquiryListRequest } = useInquiryFilterStore() const { data: inquiryList, isLoading: isLoadingInquiryList } = useQuery({ - queryKey: ['inquiryList', qnoNo, compCd, inquiryListRequest], + queryKey: ['inquiryList', inquiryListRequest], queryFn: async () => { - const resp = await axiosInstance(null).get('/api/qna/list', { - params: { inquiryListRequest }, - }) - return resp.data + try { + const resp = await axiosInstance(null).get<{ data: InquiryList[] }>(`/api/qna/list`, { + params: { inquiryListRequest }, + }) + return resp.data.data + } catch (error: any) { + console.error(error.response.data) + return [] + } }, }) const { data: inquiryDetail, isLoading: isLoadingInquiryDetail } = useQuery({ queryKey: ['inquiryDetail', qnoNo, compCd], queryFn: async () => { - const resp = await axiosInstance(null).get(`/api/qna/detail`, { - params: { qnoNo, compCd, loginId: session?.userNm }, - }) - return resp.data + try { + const resp = await axiosInstance(null).get<{ data: Inquiry }>(`/api/qna/detail`, { + params: { qnoNo, compCd, langCd: 'JA', loginId: 'x112' }, + }) + return resp.data.data + } catch (error: any) { + console.error(error.response) + return null + } }, enabled: qnoNo !== undefined && compCd !== undefined, }) const { mutateAsync: saveInquiry, isPending: isSavingInquiry } = useMutation({ mutationFn: async (inquiryRequest: InquiryRequest) => { - const resp = await axiosInstance(null).post('/api/qna/save', inquiryRequest) - return resp.data + const resp = await axiosInstance(null).post<{ data: InquirySaveResponse }>('/api/qna/save', inquiryRequest) + return resp.data.data }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['inquiryList'] }) diff --git a/src/store/inquiryFilterStore.ts b/src/store/inquiryFilterStore.ts index edfadc4..af862c2 100644 --- a/src/store/inquiryFilterStore.ts +++ b/src/store/inquiryFilterStore.ts @@ -9,33 +9,33 @@ type InquiryFilterState = { export const useInquiryFilterStore = create((set) => ({ inquiryListRequest: { - compCd: '', - langCd: '', - storeId: '', - siteTpCd: '', - schTitle: '', - schRegId: '', - schFromDt: '', - schToDt: '', + compCd: '5200', + langCd: 'JA', + storeId: 'X112', + siteTpCd: 'QC', + schTitle: null, + schRegId: null, + schFromDt: null, + schToDt: null, startRow: 0, - endRow: 0, - loginId: '', + endRow: 10, + loginId: 'x112', }, setInquiryListRequest: (inquiryListRequest) => set({ inquiryListRequest }), reset: () => set({ inquiryListRequest: { - compCd: '', - langCd: '', - storeId: '', - siteTpCd: '', + compCd: '5200', + langCd: 'JA', + storeId: 'X112', + siteTpCd: 'QC', schTitle: '', schRegId: '', schFromDt: '', schToDt: '', startRow: 0, - endRow: 0, - loginId: '', + endRow: 50, + loginId: 'x112', }, }), })) From b4dfc2211f39a41c1dbe6fde9b7562e03f0e858d Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Wed, 14 May 2025 11:06:24 +0900 Subject: [PATCH 03/19] feat: modify inquiry save requestParameter to formdata add files --- src/app/api/qna/detail/route.ts | 1 + src/app/api/qna/file/route.ts | 15 +++++++++++++++ src/app/api/qna/list/route.ts | 2 +- src/app/api/qna/save/route.ts | 20 ++++++++++++++------ src/components/inquiry/Answer.tsx | 5 +++-- src/components/inquiry/Detail.tsx | 6 +++--- src/components/inquiry/RegistForm.tsx | 6 +++--- src/hooks/useInquiry.ts | 21 ++++++++++++++++++--- 8 files changed, 58 insertions(+), 18 deletions(-) create mode 100644 src/app/api/qna/file/route.ts diff --git a/src/app/api/qna/detail/route.ts b/src/app/api/qna/detail/route.ts index c91931a..da709c0 100644 --- a/src/app/api/qna/detail/route.ts +++ b/src/app/api/qna/detail/route.ts @@ -13,6 +13,7 @@ export async function GET(request: Request) { try { const response = await axios.get(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/qna/detail?${queryStringFormatter(params)}`) + console.log('response.data detail:: ', response.data) if (response.status === 200) { return NextResponse.json(response.data) } diff --git a/src/app/api/qna/file/route.ts b/src/app/api/qna/file/route.ts new file mode 100644 index 0000000..2c4556a --- /dev/null +++ b/src/app/api/qna/file/route.ts @@ -0,0 +1,15 @@ +import axios from 'axios' +import { NextResponse } from 'next/server' + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url) + const encodeFileNo = searchParams.get('encodeFileNo') + + if (!encodeFileNo) { + return NextResponse.json({ error: 'fileNo is required' }, { status: 400 }) + } + const response = await axios.get(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/file/downloadFile?encodeFileNo=${encodeFileNo}`) + console.log('response.data:: ', response.data) + + return NextResponse.json(response.data) +} diff --git a/src/app/api/qna/list/route.ts b/src/app/api/qna/list/route.ts index 2a63a3c..901aa20 100644 --- a/src/app/api/qna/list/route.ts +++ b/src/app/api/qna/list/route.ts @@ -17,7 +17,7 @@ export async function GET(request: Request) { try { const response = await axios.get(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/qna/list?${queryStringFormatter(params)}`) - + console.log('response.data:: ', response.data) if (response.status === 200) { return NextResponse.json(response.data) } diff --git a/src/app/api/qna/save/route.ts b/src/app/api/qna/save/route.ts index e16593a..dd4edaa 100644 --- a/src/app/api/qna/save/route.ts +++ b/src/app/api/qna/save/route.ts @@ -2,13 +2,21 @@ import axios from 'axios' import { NextResponse } from 'next/server' export async function POST(request: Request) { - const body = await request.json() + const formData = await request.formData() + console.log('formData:: ', formData) + // const body = await request.json() + // console.log('body:: ', body) - const response = await axios.post(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/qna/save`, body) + try { + const response = await axios.post(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/qna/save`, formData) + console.log('response.data:: ', response.data) - if (response.status === 200) { - return NextResponse.json(response.data) + if (response.status === 200) { + return NextResponse.json(response.data) + } + return NextResponse.json({ error: response.data }, { status: response.status }) + } catch (error) { + console.error('error:: ', error) + return NextResponse.json({ error: 'Failed to save qna' }, { status: 500 }) } - - return NextResponse.json({ error: 'Failed to save qna' }, { status: response.status }) } diff --git a/src/components/inquiry/Answer.tsx b/src/components/inquiry/Answer.tsx index 485abde..4b44eb3 100644 --- a/src/components/inquiry/Answer.tsx +++ b/src/components/inquiry/Answer.tsx @@ -1,8 +1,9 @@ 'use client' +import { useInquiry } from '@/hooks/useInquiry' import { Inquiry } from '@/types/Inquiry' -export default function Answer({ inquiryDetail }: { inquiryDetail: Inquiry}) { +export default function Answer({ inquiryDetail, downloadFile }: { inquiryDetail: Inquiry; downloadFile: (encodeFileNo: number) => Promise }) { return ( <>
@@ -21,7 +22,7 @@ export default function Answer({ inquiryDetail }: { inquiryDetail: Inquiry}) {
    {inquiryDetail?.ansListFile?.map((file) => (
  • -
  • diff --git a/src/components/inquiry/Detail.tsx b/src/components/inquiry/Detail.tsx index a983bca..717b714 100644 --- a/src/components/inquiry/Detail.tsx +++ b/src/components/inquiry/Detail.tsx @@ -12,7 +12,7 @@ export default function Detail() { const params = useParams() const id = params.id - const { inquiryDetail } = useInquiry(Number(id), '5200') + const { inquiryDetail, downloadFile } = useInquiry(Number(id), '5200') const router = useRouter() return ( @@ -66,7 +66,7 @@ export default function Detail() {
      {inquiryDetail?.listFile?.map((file) => (
    • -
    • @@ -75,7 +75,7 @@ export default function Detail() {
- {inquiryDetail?.answerYn === 'Y' && inquiryDetail && } + {inquiryDetail?.answerYn === 'Y' && inquiryDetail && }
- 屋根 - 適合性 - 屋根材 -
-
屋根材適合性確認依頼
-
- 入力した内容が表示されます. -
- インストール可能であることを確認してください. -
- 屋根の写真を添付しました. + {inquiryDetail?.qnaClsLrgCd} + {inquiryDetail?.qnaClsMidCd} + {inquiryDetail?.qnaClsSmlCd}
{inquiryDetail?.qstTitle}
{inquiryDetail?.qstContents}
@@ -91,6 +69,11 @@ export default function Detail() { ))} +
  • + +
  • diff --git a/src/components/inquiry/RegistForm.tsx b/src/components/inquiry/RegistForm.tsx index 09599ea..1e97130 100644 --- a/src/components/inquiry/RegistForm.tsx +++ b/src/components/inquiry/RegistForm.tsx @@ -4,7 +4,7 @@ import { useInquiry } from '@/hooks/useInquiry' import { useSessionStore } from '@/store/session' import { InquiryRequest } from '@/types/Inquiry' -import { useEffect, useState } from 'react' +import { useState } from 'react' import { useRouter } from 'next/navigation' export default function RegistForm() { const { saveInquiry, isSavingInquiry } = useInquiry() @@ -20,14 +20,15 @@ export default function RegistForm() { compCd: '5200', siteTpCd: 'QC', qnaClsLrgCd: '', - qnaClsMidCd: 'B02', + qnaClsMidCd: '', qnaClsSmlCd: null, title: '', - contents: null, + contents: '', regId: 'X112', regUserNm: 'TEST', regUserTelNo: null, - storeId: null, + storeId: 'X112', + qstMail: '', }) const [attachedFiles, setAttachedFiles] = useState([]) @@ -45,12 +46,30 @@ export default function RegistForm() { } const handleSubmit = async () => { - if (confirm('お問い合わせを登録しますか? Hanwha Japanの担当者にお問い合わせメールが送信されます。')) { - const res = await saveInquiry({ inquiryRequest, files: attachedFiles }) - alert('保存されました。') - router.push(`/inquiry/${res.qnaNo}`) - } - return + const formData = new FormData() + attachedFiles.forEach((file) => { + formData.append('files', file) + }) + Object.entries(inquiryRequest).forEach(([key, value]) => { + formData.append(key, value ?? '') + }) + + // FormData를 객체로 변환하여 확인 + const formDataObj: Record = {} + formData.forEach((value, key) => { + formDataObj[key] = value + }) + console.log('formData contents:', formDataObj) + + window.neoConfirm( + 'お問い合わせを登録しますか? Hanwha Japanの担当者にお問い合わせメールが送信されます。', + async () => { + const res = await saveInquiry(formData) + alert('保存されました。') + router.push(`/inquiry/${res.qnaNo}`) + }, + () => null, + ) } return ( @@ -62,30 +81,42 @@ export default function RegistForm() { お問い合わせタイプ *
    - setInquiryRequest({ ...inquiryRequest, qnaClsLrgCd: e.target.value })} + > + + +
    - setInquiryRequest({ ...inquiryRequest, qnaClsMidCd: e.target.value })} + > + + +
    - setInquiryRequest({ ...inquiryRequest, qnaClsSmlCd: e.target.value })} + > + + +
    @@ -94,27 +125,36 @@ export default function RegistForm() { 名前 *
    - + setInquiryRequest({ ...inquiryRequest, regUserNm: e.target.value })} + />
    電話番号
    - + setInquiryRequest({ ...inquiryRequest, regUserTelNo: e.target.value })} + />
    - 名前 * + E-mail *
    - -
    -
    -
    -
    電話番号
    -
    - + setInquiryRequest({ ...inquiryRequest, qstMail: e.target.value })} + />
    @@ -122,7 +162,12 @@ export default function RegistForm() { お問い合わせタイトル *
    - + setInquiryRequest({ ...inquiryRequest, title: e.target.value })} + />
    diff --git a/src/components/inquiry/list/ListForm.tsx b/src/components/inquiry/list/ListForm.tsx index d22d424..0118f58 100644 --- a/src/components/inquiry/list/ListForm.tsx +++ b/src/components/inquiry/list/ListForm.tsx @@ -5,8 +5,8 @@ import { useState } from 'react' export default function ListForm() { const router = useRouter() - const [searchKeyword, setSearchKeyword] = useState('') const { inquiryListRequest, setInquiryListRequest } = useInquiryFilterStore() + const [searchKeyword, setSearchKeyword] = useState(inquiryListRequest.schTitle ?? '') const handleSearch = () => { if (searchKeyword.length >= 2) { diff --git a/src/components/inquiry/list/ListTable.tsx b/src/components/inquiry/list/ListTable.tsx index 9598117..eed1d34 100644 --- a/src/components/inquiry/list/ListTable.tsx +++ b/src/components/inquiry/list/ListTable.tsx @@ -7,6 +7,7 @@ import { InquiryList } from '@/types/Inquiry' import { useRouter } from 'next/navigation' import { useInquiryFilterStore } from '@/store/inquiryFilterStore' import { useSessionStore } from '@/store/session' +import ListForm from './ListForm' const badgeStyle = [ { @@ -29,14 +30,23 @@ export default function ListTable() { const { inquiryList } = useInquiry() const { inquiryListRequest, setInquiryListRequest } = useInquiryFilterStore() + const [heldInquiryList, setHeldInquiryList] = useState([]) + const { session } = useSessionStore() useEffect(() => { - if (inquiryList.length !== 0) { - const hasMoreItems = inquiryList[0].totCnt > offset + 10 - setHasMore(hasMoreItems) + if (inquiryList.length > 0) { + if (offset === 0) { + setHeldInquiryList(inquiryList) + } else { + const remainingList = heldInquiryList.slice(offset, offset + 10) + if (JSON.stringify(remainingList) !== JSON.stringify(inquiryList)) { + setHeldInquiryList((prev) => [...prev, ...inquiryList]) + } + } + setHasMore(inquiryList.length > offset + 10) } else { - setHasMore(false) + setHeldInquiryList([]) } }, [inquiryList, offset]) @@ -49,8 +59,19 @@ export default function ListTable() { setInquiryListRequest({ ...inquiryListRequest, startRow: offset, endRow: offset + 10 }) } + const handleFilter = (e: React.ChangeEvent) => { + console.log(e.target.value) + setHeldInquiryList(inquiryList.filter((inquiry: InquiryList) => inquiry.answerYn === e.target.value)) + if (e.target.value === '') { + setHeldInquiryList(inquiryList) + } + } + + console.log('heldInquiryList:: ', heldInquiryList) + return ( <> +
    @@ -60,7 +81,7 @@ export default function ListTable() {
    - handleFilter(e)}> @@ -69,11 +90,11 @@ export default function ListTable() {
    - 合計 {inquiryList.length}個 + 合計 {heldInquiryList.length > 0 ? heldInquiryList[0].totCnt : 0}
      - {inquiryList.length > 0 && - inquiryList.map((inquiry: InquiryList) => ( + {heldInquiryList.length > 0 && + heldInquiryList.map((inquiry: InquiryList) => (
    • router.push(`/inquiry/${inquiry.qnaNo}`)}>
      {inquiry.qnaClsLrgCd}
      diff --git a/src/hooks/useInquiry.ts b/src/hooks/useInquiry.ts index 1314dee..0e698b1 100644 --- a/src/hooks/useInquiry.ts +++ b/src/hooks/useInquiry.ts @@ -12,10 +12,10 @@ export function useInquiry( inquiryDetail: Inquiry | null isLoadingInquiryDetail: boolean isSavingInquiry: boolean - saveInquiry: (params: { inquiryRequest: InquiryRequest; files: File[] }) => Promise + saveInquiry: (formData: FormData) => Promise downloadFile: (encodeFileNo: number) => Promise } { - const { session } = useSessionStore() + // const { session } = useSessionStore() const queryClient = useQueryClient() const { inquiryListRequest } = useInquiryFilterStore() @@ -41,6 +41,7 @@ export function useInquiry( const resp = await axiosInstance(null).get<{ data: Inquiry }>(`/api/qna/detail`, { params: { qnoNo, compCd, langCd: 'JA', loginId: 'x112' }, }) + console.log('resp.data.data:: ', resp.data.data) return resp.data.data } catch (error: any) { console.error(error.response) @@ -51,15 +52,7 @@ export function useInquiry( }) const { mutateAsync: saveInquiry, isPending: isSavingInquiry } = useMutation({ - mutationFn: async ({ inquiryRequest, files }: { inquiryRequest: InquiryRequest; files: File[] }) => { - const formData = new FormData() - Object.entries(inquiryRequest).forEach(([key, value]) => { - formData.append(key, value ?? '') - }) - files.forEach((file) => { - formData.append('files', file) - }) - + mutationFn: async (formData: FormData) => { const resp = await axiosInstance(null).post<{ data: InquirySaveResponse }>('/api/qna/save', formData) return resp.data.data }, diff --git a/src/types/Inquiry.ts b/src/types/Inquiry.ts index 96a6fff..c876020 100644 --- a/src/types/Inquiry.ts +++ b/src/types/Inquiry.ts @@ -75,11 +75,12 @@ export type InquiryRequest = { qnaClsMidCd: string //qna CLS Mid Code qnaClsSmlCd: string | null //qna CLS Small Code title: string //title - contents: string | null //contents + contents: string //contents regId: string //registration Userid - storeId: string | null //store id + storeId: string //store id regUserNm: string //registration User name regUserTelNo: string | null //registration User tel number + qstMail: string //mail } export type InquirySaveResponse = { From 1d77fec86d8bfd2da87115b20573144d72ce6b6c Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Fri, 16 May 2025 14:36:41 +0900 Subject: [PATCH 05/19] fix: change inquiry save parameter type to form data --- src/app/api/qna/save/route.ts | 14 ++++++++------ src/components/inquiry/Detail.tsx | 5 ----- src/components/inquiry/list/ListTable.tsx | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/app/api/qna/save/route.ts b/src/app/api/qna/save/route.ts index 00633bd..3afc339 100644 --- a/src/app/api/qna/save/route.ts +++ b/src/app/api/qna/save/route.ts @@ -3,17 +3,19 @@ import { NextResponse } from 'next/server' export async function POST(request: Request) { const formData = await request.formData() - console.log('formData:: ', formData) - try { - const response = await axios.post(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/qna/save`, formData) - + const response = await axios.post(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/qna/save`, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }) + console.log('response:: ', response) if (response.status === 200) { return NextResponse.json(response.data) } return NextResponse.json({ error: response.data }, { status: response.status }) - } catch (error) { - console.error('error:: ', error) + } catch (error: any) { + console.error('error:: ', error.response) return NextResponse.json({ error: 'Failed to save qna' }, { status: 500 }) } } diff --git a/src/components/inquiry/Detail.tsx b/src/components/inquiry/Detail.tsx index 9bb22b4..850cc6d 100644 --- a/src/components/inquiry/Detail.tsx +++ b/src/components/inquiry/Detail.tsx @@ -69,11 +69,6 @@ export default function Detail() {
    • ))} -
    • - -
    diff --git a/src/components/inquiry/list/ListTable.tsx b/src/components/inquiry/list/ListTable.tsx index eed1d34..c12cf61 100644 --- a/src/components/inquiry/list/ListTable.tsx +++ b/src/components/inquiry/list/ListTable.tsx @@ -48,7 +48,7 @@ export default function ListTable() { } else { setHeldInquiryList([]) } - }, [inquiryList, offset]) + }, [inquiryList, offset, setHeldInquiryList]) const handleMyInquiry = () => { setInquiryListRequest({ ...inquiryListRequest, schRegId: inquiryListRequest.schRegId ? null : session.userId }) From 96c725b45914a0fef76b6ae1a726cf06b6aeca07 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Fri, 16 May 2025 15:58:17 +0900 Subject: [PATCH 06/19] feat: implement inquriy file upload --- .env.development | 3 ++- src/app/api/qna/detail/route.ts | 2 +- src/components/inquiry/Detail.tsx | 2 +- src/components/inquiry/list/ListTable.tsx | 25 +++++++++-------------- src/hooks/useInquiry.ts | 7 +++---- 5 files changed, 17 insertions(+), 22 deletions(-) diff --git a/.env.development b/.env.development index 5aa380f..fb9112b 100644 --- a/.env.development +++ b/.env.development @@ -8,7 +8,8 @@ NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 #1:1문의 api # NEXT_PUBLIC_INQUIRY_API_URL=http://1.248.227.176:38080 -NEXT_PUBLIC_INQUIRY_API_URL=http://172.23.4.129:8110 +# NEXT_PUBLIC_INQUIRY_API_URL=http://172.23.4.129:8110 +NEXT_PUBLIC_INQUIRY_API_URL=http://172.30.1.93:8120 #QPARTNER 로그인 api diff --git a/src/app/api/qna/detail/route.ts b/src/app/api/qna/detail/route.ts index da709c0..124ec68 100644 --- a/src/app/api/qna/detail/route.ts +++ b/src/app/api/qna/detail/route.ts @@ -6,7 +6,7 @@ export async function GET(request: Request) { const { searchParams } = new URL(request.url) const params = { compCd: searchParams.get('compCd'), - qnoNo: searchParams.get('qnoNo'), + qnaNo: searchParams.get('qnoNo'), langCd: searchParams.get('langCd'), loginId: searchParams.get('loginId'), } diff --git a/src/components/inquiry/Detail.tsx b/src/components/inquiry/Detail.tsx index 850cc6d..29482d4 100644 --- a/src/components/inquiry/Detail.tsx +++ b/src/components/inquiry/Detail.tsx @@ -63,7 +63,7 @@ export default function Detail() {
    ファイル添付
      {inquiryDetail?.listFile?.map((file) => ( -
    • +
    • diff --git a/src/components/inquiry/list/ListTable.tsx b/src/components/inquiry/list/ListTable.tsx index c12cf61..454dbb3 100644 --- a/src/components/inquiry/list/ListTable.tsx +++ b/src/components/inquiry/list/ListTable.tsx @@ -23,7 +23,7 @@ const badgeStyle = [ ] export default function ListTable() { const [offset, setOffset] = useState(0) - const [hasMore, setHasMore] = useState(true) + const [hasMore, setHasMore] = useState(false) const router = useRouter() @@ -35,20 +35,13 @@ export default function ListTable() { const { session } = useSessionStore() useEffect(() => { - if (inquiryList.length > 0) { - if (offset === 0) { - setHeldInquiryList(inquiryList) - } else { - const remainingList = heldInquiryList.slice(offset, offset + 10) - if (JSON.stringify(remainingList) !== JSON.stringify(inquiryList)) { - setHeldInquiryList((prev) => [...prev, ...inquiryList]) - } - } - setHasMore(inquiryList.length > offset + 10) - } else { - setHeldInquiryList([]) - } - }, [inquiryList, offset, setHeldInquiryList]) + if (!inquiryList) return + setHeldInquiryList(inquiryList) + setHasMore(inquiryList.length > offset + 10) + }, [inquiryList, offset]) + + console.log('heldInquiryList:: ', heldInquiryList) + console.log('inquiryList:: ', inquiryList) const handleMyInquiry = () => { setInquiryListRequest({ ...inquiryListRequest, schRegId: inquiryListRequest.schRegId ? null : session.userId }) @@ -57,6 +50,8 @@ export default function ListTable() { const handleLoadMore = () => { setOffset(offset + 10) setInquiryListRequest({ ...inquiryListRequest, startRow: offset, endRow: offset + 10 }) + + setHeldInquiryList((prev) => [...prev, ...inquiryList]) } const handleFilter = (e: React.ChangeEvent) => { diff --git a/src/hooks/useInquiry.ts b/src/hooks/useInquiry.ts index 0e698b1..c49df3e 100644 --- a/src/hooks/useInquiry.ts +++ b/src/hooks/useInquiry.ts @@ -1,8 +1,8 @@ -import { InquiryList, Inquiry, InquiryRequest, InquirySaveResponse } from '@/types/Inquiry' +import { InquiryList, Inquiry, InquirySaveResponse } from '@/types/Inquiry' import { axiosInstance } from '@/libs/axios' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useInquiryFilterStore } from '@/store/inquiryFilterStore' -import { useSessionStore } from '@/store/session' + export function useInquiry( qnoNo?: number, compCd?: string, @@ -15,7 +15,6 @@ export function useInquiry( saveInquiry: (formData: FormData) => Promise downloadFile: (encodeFileNo: number) => Promise } { - // const { session } = useSessionStore() const queryClient = useQueryClient() const { inquiryListRequest } = useInquiryFilterStore() From b0878c853bc7f5128a37d79a7d6d789d37f15abd Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Thu, 22 May 2025 14:26:51 +0900 Subject: [PATCH 07/19] feat: add inquiry types code option & filtering inquiry list by answer exist --- .env.development | 4 +- src/app/api/qna/detail/route.ts | 1 - src/app/api/qna/file/route.ts | 4 +- src/app/api/qna/list/route.ts | 1 - src/app/api/qna/save/route.ts | 1 - src/components/inquiry/Answer.tsx | 6 +- src/components/inquiry/RegistForm.tsx | 67 +++++++++---- src/components/inquiry/list/ListForm.tsx | 1 - src/components/inquiry/list/ListTable.tsx | 114 ++++++++++++++-------- src/hooks/useInquiry.ts | 22 ++++- src/store/inquiryFilterStore.ts | 16 +-- src/types/Inquiry.ts | 1 + 12 files changed, 153 insertions(+), 85 deletions(-) diff --git a/.env.development b/.env.development index 4e7f5a9..6bc3000 100644 --- a/.env.development +++ b/.env.development @@ -8,8 +8,8 @@ NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 #1:1문의 api # NEXT_PUBLIC_INQUIRY_API_URL=http://1.248.227.176:38080 -# NEXT_PUBLIC_INQUIRY_API_URL=http://172.23.4.129:8110 -NEXT_PUBLIC_INQUIRY_API_URL=http://172.30.1.93:8120 +NEXT_PUBLIC_INQUIRY_API_URL=http://172.23.4.129:8110 +# NEXT_PUBLIC_INQUIRY_API_URL=http://172.30.1.93:8120 #QPARTNER 로그인 api diff --git a/src/app/api/qna/detail/route.ts b/src/app/api/qna/detail/route.ts index 124ec68..315c18e 100644 --- a/src/app/api/qna/detail/route.ts +++ b/src/app/api/qna/detail/route.ts @@ -13,7 +13,6 @@ export async function GET(request: Request) { try { const response = await axios.get(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/qna/detail?${queryStringFormatter(params)}`) - console.log('response.data detail:: ', response.data) if (response.status === 200) { return NextResponse.json(response.data) } diff --git a/src/app/api/qna/file/route.ts b/src/app/api/qna/file/route.ts index 13a9232..a122da6 100644 --- a/src/app/api/qna/file/route.ts +++ b/src/app/api/qna/file/route.ts @@ -9,13 +9,11 @@ export async function GET(request: Request) { return NextResponse.json({ error: 'encodeFileNo is required' }, { status: 400 }) } try { - const response = await axios.get(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/file/downloadFile`, { + const response = await axios.get(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/file/downloadFile`, { params: { encodeFileNo, }, }) - console.log('response.data:: ', response.data) - return NextResponse.json(response.data) } catch (error: any) { console.error(error.response) diff --git a/src/app/api/qna/list/route.ts b/src/app/api/qna/list/route.ts index 2f7a52a..f793b98 100644 --- a/src/app/api/qna/list/route.ts +++ b/src/app/api/qna/list/route.ts @@ -17,7 +17,6 @@ export async function GET(request: Request) { try { const response = await axios.get(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/qna/list?${queryStringFormatter(params)}`) - console.log('response.data:: ', response.data) if (response.status === 200) { return NextResponse.json(response.data) } diff --git a/src/app/api/qna/save/route.ts b/src/app/api/qna/save/route.ts index 3afc339..f51ae66 100644 --- a/src/app/api/qna/save/route.ts +++ b/src/app/api/qna/save/route.ts @@ -9,7 +9,6 @@ export async function POST(request: Request) { 'Content-Type': 'multipart/form-data', }, }) - console.log('response:: ', response) if (response.status === 200) { return NextResponse.json(response.data) } diff --git a/src/components/inquiry/Answer.tsx b/src/components/inquiry/Answer.tsx index 94b6152..76da68a 100644 --- a/src/components/inquiry/Answer.tsx +++ b/src/components/inquiry/Answer.tsx @@ -1,6 +1,5 @@ 'use client' -import { useInquiry } from '@/hooks/useInquiry' import { Inquiry } from '@/types/Inquiry' export default function Answer({ inquiryDetail, downloadFile }: { inquiryDetail: Inquiry; downloadFile: (encodeFileNo: number) => Promise }) { @@ -15,10 +14,7 @@ export default function Answer({ inquiryDetail, downloadFile }: { inquiryDetail:
    回答
    -
    - 一次側接続は, 自動切替開閉器と住宅分電盤昼間遮断器との間に蓄電システム遮断器を配線する方法です. 二次側接続は, - 住宅分電盤週間ブレーカの二次側に蓄電システムブレーカを接続する -
    +
    {inquiryDetail?.ansContents}
    ファイル添付
    diff --git a/src/components/inquiry/RegistForm.tsx b/src/components/inquiry/RegistForm.tsx index 1e97130..dd02baa 100644 --- a/src/components/inquiry/RegistForm.tsx +++ b/src/components/inquiry/RegistForm.tsx @@ -4,18 +4,13 @@ import { useInquiry } from '@/hooks/useInquiry' import { useSessionStore } from '@/store/session' import { InquiryRequest } from '@/types/Inquiry' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' export default function RegistForm() { const { saveInquiry, isSavingInquiry } = useInquiry() const { session } = useSessionStore() const router = useRouter() - // TODO: 세션 정보 적용 | 현재는 test용 정보 적용 - // useEffect(() => { - // setInquiryRequest({ ...inquiryRequest, regId: session?.userId ?? '', regUserNm: session?.userNm ?? '' }) - // }, [session]) - const [inquiryRequest, setInquiryRequest] = useState({ compCd: '5200', siteTpCd: 'QC', @@ -24,12 +19,26 @@ export default function RegistForm() { qnaClsSmlCd: null, title: '', contents: '', - regId: 'X112', - regUserNm: 'TEST', + regId: '', + regUserNm: '', regUserTelNo: null, - storeId: 'X112', + storeId: '', qstMail: '', }) + const requiredFieldNames = [ + { id: 'qnaClsLrgCd', name: 'お問い合わせタイプ' }, + { id: 'qnaClsMidCd', name: 'お問い合わせタイプ' }, + { id: 'regUserNm', name: '名前' }, + { id: 'qstMail', name: 'E-mail' }, + { id: 'title', name: 'お問い合わせタイトル' }, + { id: 'contents', name: 'お問い合わせ内容' }, + ] + + useEffect(() => { + if (session?.isLoggedIn) { + setInquiryRequest({ ...inquiryRequest, regId: session?.userId ?? '', regUserNm: session?.userNm ?? '', storeId: session?.storeId ?? '' }) + } + }, [session]) const [attachedFiles, setAttachedFiles] = useState([]) @@ -45,7 +54,25 @@ export default function RegistForm() { setAttachedFiles(attachedFiles.filter((_, i) => i !== index)) } + const focusOnRequiredField = (fieldId: string) => { + const element = document.getElementById(fieldId) + if (element) element.focus() + } + const handleSubmit = async () => { + const emptyField = requiredFieldNames.find((field) => inquiryRequest[field.id as keyof InquiryRequest] === '') + if (emptyField) { + alert(`${emptyField?.name}を入力してください。`) + focusOnRequiredField(emptyField?.id ?? '') + return + } + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!emailRegex.test(inquiryRequest.qstMail)) { + alert('有効なメールアドレスを入力してください。') + focusOnRequiredField('qstMail') + return + } + const formData = new FormData() attachedFiles.forEach((file) => { formData.append('files', file) @@ -54,12 +81,10 @@ export default function RegistForm() { formData.append(key, value ?? '') }) - // FormData를 객체로 변환하여 확인 const formDataObj: Record = {} formData.forEach((value, key) => { formDataObj[key] = value }) - console.log('formData contents:', formDataObj) window.neoConfirm( 'お問い合わせを登録しますか? Hanwha Japanの担当者にお問い合わせメールが送信されます。', @@ -83,40 +108,40 @@ export default function RegistForm() {
    @@ -130,6 +155,8 @@ export default function RegistForm() { type="text" placeholder="名前を書いてください" onChange={(e) => setInquiryRequest({ ...inquiryRequest, regUserNm: e.target.value })} + value={inquiryRequest.regUserNm} + id="regUserNm" /> @@ -141,6 +168,7 @@ export default function RegistForm() { type="text" placeholder="電話番号を書き留めてください" onChange={(e) => setInquiryRequest({ ...inquiryRequest, regUserTelNo: e.target.value })} + id="regUserTelNo" /> @@ -154,6 +182,7 @@ export default function RegistForm() { type="text" placeholder="E-mailを書いてください" onChange={(e) => setInquiryRequest({ ...inquiryRequest, qstMail: e.target.value })} + id="qstMail" /> @@ -167,6 +196,7 @@ export default function RegistForm() { type="text" placeholder="お問い合わせタイトルを記入してください" onChange={(e) => setInquiryRequest({ ...inquiryRequest, title: e.target.value })} + id="title" /> @@ -178,8 +208,7 @@ export default function RegistForm() { diff --git a/src/components/inquiry/list/ListForm.tsx b/src/components/inquiry/list/ListForm.tsx index 0118f58..bb41fb3 100644 --- a/src/components/inquiry/list/ListForm.tsx +++ b/src/components/inquiry/list/ListForm.tsx @@ -13,7 +13,6 @@ export default function ListForm() { setInquiryListRequest({ ...inquiryListRequest, schTitle: searchKeyword }) } } - const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { handleSearch() diff --git a/src/components/inquiry/list/ListTable.tsx b/src/components/inquiry/list/ListTable.tsx index 454dbb3..bf5ce88 100644 --- a/src/components/inquiry/list/ListTable.tsx +++ b/src/components/inquiry/list/ListTable.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react' import LoadMoreButton from '../../LoadMoreButton' import { useInquiry } from '@/hooks/useInquiry' import { InquiryList } from '@/types/Inquiry' -import { useRouter } from 'next/navigation' +import { usePathname, useRouter } from 'next/navigation' import { useInquiryFilterStore } from '@/store/inquiryFilterStore' import { useSessionStore } from '@/store/session' import ListForm from './ListForm' @@ -22,48 +22,66 @@ const badgeStyle = [ }, ] export default function ListTable() { - const [offset, setOffset] = useState(0) - const [hasMore, setHasMore] = useState(false) - const router = useRouter() + const pathname = usePathname() const { inquiryList } = useInquiry() - const { inquiryListRequest, setInquiryListRequest } = useInquiryFilterStore() + const { inquiryListRequest, setInquiryListRequest, reset } = useInquiryFilterStore() - const [heldInquiryList, setHeldInquiryList] = useState([]) + const [offset, setOffset] = useState(inquiryListRequest.startRow) + const [hasMore, setHasMore] = useState(false) + + const [heldInquiryList, setHeldInquiryList] = useState([]) const { session } = useSessionStore() useEffect(() => { - if (!inquiryList) return - setHeldInquiryList(inquiryList) - setHasMore(inquiryList.length > offset + 10) - }, [inquiryList, offset]) + setInquiryListRequest({ ...inquiryListRequest, startRow: 1, endRow: 10 }) + setHeldInquiryList([]) + setOffset(1) + setHasMore(false) + }, [pathname]) - console.log('heldInquiryList:: ', heldInquiryList) - console.log('inquiryList:: ', inquiryList) + useEffect(() => { + if (!session.isLoggedIn || !inquiryList) return + if (session.isLoggedIn) { + setInquiryListRequest({ ...inquiryListRequest, storeId: session.storeId ?? '', loginId: session.userId ?? '' }) + // setInquiryListRequest({ ...inquiryListRequest, storeId: 'X112', loginId: 'x112' }) + } + console.log('inquiryListRequest', inquiryListRequest) + if (inquiryList.length > 0 && inquiryList[0].totCnt > 0) { + if (inquiryListRequest.startRow > 1) { + const isDuplicate = inquiryList.every((newItem) => heldInquiryList.some((existingItem) => existingItem.qnaNo === newItem.qnaNo)) + if (isDuplicate) return + setHeldInquiryList((prev) => [...prev, ...inquiryList]) + } else { + setHeldInquiryList(inquiryList) + } + setHasMore(inquiryList[0].totCnt > inquiryListRequest.endRow) + } else { + setHeldInquiryList([]) + setHasMore(false) + } + }, [session, inquiryList, inquiryListRequest.startRow]) const handleMyInquiry = () => { setInquiryListRequest({ ...inquiryListRequest, schRegId: inquiryListRequest.schRegId ? null : session.userId }) } - const handleLoadMore = () => { - setOffset(offset + 10) - setInquiryListRequest({ ...inquiryListRequest, startRow: offset, endRow: offset + 10 }) - - setHeldInquiryList((prev) => [...prev, ...inquiryList]) - } - const handleFilter = (e: React.ChangeEvent) => { - console.log(e.target.value) - setHeldInquiryList(inquiryList.filter((inquiry: InquiryList) => inquiry.answerYn === e.target.value)) - if (e.target.value === '') { - setHeldInquiryList(inquiryList) + switch (e.target.value) { + case 'N': + setInquiryListRequest({ ...inquiryListRequest, schAnswerYn: 'N' }) + break + case 'Y': + setInquiryListRequest({ ...inquiryListRequest, schAnswerYn: 'Y' }) + break + default: + reset() + break } } - console.log('heldInquiryList:: ', heldInquiryList) - return ( <> @@ -87,23 +105,39 @@ export default function ListTable() {
    合計 {heldInquiryList.length > 0 ? heldInquiryList[0].totCnt : 0}
    -
      - {heldInquiryList.length > 0 && - heldInquiryList.map((inquiry: InquiryList) => ( -
    • router.push(`/inquiry/${inquiry.qnaNo}`)}> -
      -
      {inquiry.qnaClsLrgCd}
      -
      {inquiry.qstTitle}
      -
      {inquiry.regDt}
      -
      badge.id === inquiry.answerYn)?.color}`}> - {badgeStyle.find((badge) => badge.id === inquiry.answerYn)?.label} + {heldInquiryList.length === 0 || (heldInquiryList.length > 0 && heldInquiryList[0].totCnt === 0) ? ( +
      +
      照会されたデータがありません。
      +
      + ) : ( +
        + {heldInquiryList.length > 0 && + heldInquiryList.map((inquiry: InquiryList) => ( +
      • router.push(`/inquiry/${inquiry.qnaNo}`)}> +
        +
        + {inquiry.qnaClsLrgCd} + {inquiry.qnaClsMidCd} + {inquiry.qnaClsSmlCd} +
        +
        {inquiry.qstTitle}
        +
        {inquiry.regDt}
        +
        badge.id === inquiry.answerYn)?.color}`}> + {badgeStyle.find((badge) => badge.id === inquiry.answerYn)?.label} +
        -
      -
    • - ))} -
    + + ))} + + )}
    - handleLoadMore()} /> + { + setInquiryListRequest({ ...inquiryListRequest, startRow: offset + 10, endRow: offset + 19 }) + setOffset(inquiryListRequest.startRow) + }} + />
    diff --git a/src/hooks/useInquiry.ts b/src/hooks/useInquiry.ts index c49df3e..a497806 100644 --- a/src/hooks/useInquiry.ts +++ b/src/hooks/useInquiry.ts @@ -1,7 +1,9 @@ import { InquiryList, Inquiry, InquirySaveResponse } from '@/types/Inquiry' import { axiosInstance } from '@/libs/axios' -import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useInquiryFilterStore } from '@/store/inquiryFilterStore' +import { useMemo } from 'react' +import { useSessionStore } from '@/store/session' export function useInquiry( qnoNo?: number, @@ -17,6 +19,7 @@ export function useInquiry( } { const queryClient = useQueryClient() const { inquiryListRequest } = useInquiryFilterStore() + const { session } = useSessionStore() const { data: inquiryList, isLoading: isLoadingInquiryList } = useQuery({ queryKey: ['inquiryList', inquiryListRequest], @@ -31,16 +34,25 @@ export function useInquiry( return [] } }, + placeholderData: (previousData) => previousData, }) + const inquriyListData = useMemo(() => { + if (isLoadingInquiryList) { + return { inquiryList: inquiryList ?? [] } + } + return { + inquiryList: inquiryList ?? [], + } + }, [inquiryList, isLoadingInquiryList]) + const { data: inquiryDetail, isLoading: isLoadingInquiryDetail } = useQuery({ - queryKey: ['inquiryDetail', qnoNo, compCd], + queryKey: ['inquiryDetail', qnoNo, compCd, session?.userId], queryFn: async () => { try { const resp = await axiosInstance(null).get<{ data: Inquiry }>(`/api/qna/detail`, { - params: { qnoNo, compCd, langCd: 'JA', loginId: 'x112' }, + params: { qnoNo, compCd, langCd: 'JA', loginId: session?.userId ?? '' }, }) - console.log('resp.data.data:: ', resp.data.data) return resp.data.data } catch (error: any) { console.error(error.response) @@ -66,7 +78,7 @@ export function useInquiry( } return { - inquiryList: inquiryList ?? [], + inquiryList: inquriyListData.inquiryList, inquiryDetail: inquiryDetail ?? null, isLoadingInquiryList, isLoadingInquiryDetail, diff --git a/src/store/inquiryFilterStore.ts b/src/store/inquiryFilterStore.ts index af862c2..62f70df 100644 --- a/src/store/inquiryFilterStore.ts +++ b/src/store/inquiryFilterStore.ts @@ -11,15 +11,16 @@ export const useInquiryFilterStore = create((set) => ({ inquiryListRequest: { compCd: '5200', langCd: 'JA', - storeId: 'X112', + storeId: '', siteTpCd: 'QC', schTitle: null, schRegId: null, schFromDt: null, schToDt: null, - startRow: 0, + schAnswerYn: null, + startRow: 1, endRow: 10, - loginId: 'x112', + loginId: '', }, setInquiryListRequest: (inquiryListRequest) => set({ inquiryListRequest }), reset: () => @@ -27,15 +28,16 @@ export const useInquiryFilterStore = create((set) => ({ inquiryListRequest: { compCd: '5200', langCd: 'JA', - storeId: 'X112', + storeId: '', siteTpCd: 'QC', schTitle: '', schRegId: '', schFromDt: '', schToDt: '', - startRow: 0, - endRow: 50, - loginId: 'x112', + schAnswerYn: null, + startRow: 1, + endRow: 10, + loginId: '', }, }), })) diff --git a/src/types/Inquiry.ts b/src/types/Inquiry.ts index c876020..79ac368 100644 --- a/src/types/Inquiry.ts +++ b/src/types/Inquiry.ts @@ -7,6 +7,7 @@ export type InquiryListRequest = { schRegId: string | null //search regId schFromDt: string | null //search start date schToDt: string | null //search end date + schAnswerYn: string | null //search answer yn startRow: number //start row endRow: number //end row loginId: string //login id From 333f06bb642e0e2aaf4a8136d27e3299c87abfb8 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Thu, 22 May 2025 14:31:25 +0900 Subject: [PATCH 08/19] fix : changed to using axios in custom hooks to apply interceptor from axios --- src/hooks/useInquiry.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hooks/useInquiry.ts b/src/hooks/useInquiry.ts index a497806..816f31d 100644 --- a/src/hooks/useInquiry.ts +++ b/src/hooks/useInquiry.ts @@ -1,5 +1,5 @@ import { InquiryList, Inquiry, InquirySaveResponse } from '@/types/Inquiry' -import { axiosInstance } from '@/libs/axios' +import { useAxios } from '@/hooks/useAxios' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useInquiryFilterStore } from '@/store/inquiryFilterStore' import { useMemo } from 'react' @@ -20,6 +20,7 @@ export function useInquiry( const queryClient = useQueryClient() const { inquiryListRequest } = useInquiryFilterStore() const { session } = useSessionStore() + const { axiosInstance } = useAxios() const { data: inquiryList, isLoading: isLoadingInquiryList } = useQuery({ queryKey: ['inquiryList', inquiryListRequest], From 50a2335590ab03ad1b42343ad25575ce83a90af2 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Thu, 22 May 2025 18:13:16 +0900 Subject: [PATCH 09/19] feat: get inquiry type common code --- src/app/api/qna/route.ts | 19 ++++++++++++ src/components/inquiry/RegistForm.tsx | 35 +++++++++++++++++------ src/components/inquiry/list/ListTable.tsx | 2 -- src/hooks/useInquiry.ts | 14 ++++++++- src/types/Inquiry.ts | 7 +++++ 5 files changed, 65 insertions(+), 12 deletions(-) create mode 100644 src/app/api/qna/route.ts diff --git a/src/app/api/qna/route.ts b/src/app/api/qna/route.ts new file mode 100644 index 0000000..994a86a --- /dev/null +++ b/src/app/api/qna/route.ts @@ -0,0 +1,19 @@ +import { NextResponse } from 'next/server' +import axios from 'axios' +import { CommonCode } from '@/types/Inquiry' + +export async function GET() { + const response = await axios.get(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/system/commonCodeListData`) + const codeList: CommonCode[] = [] + response.data.data.apiCommCdList.forEach((item: any) => { + if (item.headCd === '204200' || item.headCd === '204300' || item.headCd === '204400') { + codeList.push({ + headCd: item.headCd, + code: item.code, + name: item.codeJp, + refChar1: item.refChr1, + }) + } + }) + return NextResponse.json({ data: codeList }) +} diff --git a/src/components/inquiry/RegistForm.tsx b/src/components/inquiry/RegistForm.tsx index dd02baa..86d2ec9 100644 --- a/src/components/inquiry/RegistForm.tsx +++ b/src/components/inquiry/RegistForm.tsx @@ -40,6 +40,8 @@ export default function RegistForm() { } }, [session]) + const { commonCodeList } = useInquiry() + const [attachedFiles, setAttachedFiles] = useState([]) const handleFileChange = (e: React.ChangeEvent) => { @@ -113,9 +115,14 @@ export default function RegistForm() { value={inquiryRequest.qnaClsLrgCd} onChange={(e) => setInquiryRequest({ ...inquiryRequest, qnaClsLrgCd: e.target.value })} > - - - + + {commonCodeList + .filter((code) => code.headCd === '204200') + .map((code) => ( + + ))}
    @@ -126,9 +133,14 @@ export default function RegistForm() { value={inquiryRequest.qnaClsMidCd} onChange={(e) => setInquiryRequest({ ...inquiryRequest, qnaClsMidCd: e.target.value })} > - - - + + {commonCodeList + .filter((code) => code.refChar1 === inquiryRequest.qnaClsLrgCd) + .map((code) => ( + + ))}
    @@ -139,9 +151,14 @@ export default function RegistForm() { value={inquiryRequest.qnaClsSmlCd ?? ''} onChange={(e) => setInquiryRequest({ ...inquiryRequest, qnaClsSmlCd: e.target.value })} > - - - + + {commonCodeList + .filter((code) => code.refChar1 === inquiryRequest.qnaClsMidCd) + .map((code) => ( + + ))}
    diff --git a/src/components/inquiry/list/ListTable.tsx b/src/components/inquiry/list/ListTable.tsx index bf5ce88..f7d874b 100644 --- a/src/components/inquiry/list/ListTable.tsx +++ b/src/components/inquiry/list/ListTable.tsx @@ -46,9 +46,7 @@ export default function ListTable() { if (!session.isLoggedIn || !inquiryList) return if (session.isLoggedIn) { setInquiryListRequest({ ...inquiryListRequest, storeId: session.storeId ?? '', loginId: session.userId ?? '' }) - // setInquiryListRequest({ ...inquiryListRequest, storeId: 'X112', loginId: 'x112' }) } - console.log('inquiryListRequest', inquiryListRequest) if (inquiryList.length > 0 && inquiryList[0].totCnt > 0) { if (inquiryListRequest.startRow > 1) { const isDuplicate = inquiryList.every((newItem) => heldInquiryList.some((existingItem) => existingItem.qnaNo === newItem.qnaNo)) diff --git a/src/hooks/useInquiry.ts b/src/hooks/useInquiry.ts index 816f31d..3806ec1 100644 --- a/src/hooks/useInquiry.ts +++ b/src/hooks/useInquiry.ts @@ -1,4 +1,4 @@ -import { InquiryList, Inquiry, InquirySaveResponse } from '@/types/Inquiry' +import { InquiryList, Inquiry, InquirySaveResponse, CommonCode } from '@/types/Inquiry' import { useAxios } from '@/hooks/useAxios' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useInquiryFilterStore } from '@/store/inquiryFilterStore' @@ -16,6 +16,7 @@ export function useInquiry( isSavingInquiry: boolean saveInquiry: (formData: FormData) => Promise downloadFile: (encodeFileNo: number) => Promise + commonCodeList: CommonCode[] } { const queryClient = useQueryClient() const { inquiryListRequest } = useInquiryFilterStore() @@ -78,6 +79,16 @@ export function useInquiry( return resp.data } + const { data: commonCodeList, isLoading: isLoadingCommonCodeList } = useQuery({ + queryKey: ['commonCodeList'], + queryFn: async () => { + const resp = await axiosInstance(null).get<{ data: CommonCode[] }>(`/api/qna`) + return resp.data + }, + staleTime: Infinity, + gcTime: Infinity, + }) + return { inquiryList: inquriyListData.inquiryList, inquiryDetail: inquiryDetail ?? null, @@ -86,5 +97,6 @@ export function useInquiry( isSavingInquiry, saveInquiry, downloadFile, + commonCodeList: commonCodeList?.data ?? [], } } diff --git a/src/types/Inquiry.ts b/src/types/Inquiry.ts index 79ac368..475ed79 100644 --- a/src/types/Inquiry.ts +++ b/src/types/Inquiry.ts @@ -89,3 +89,10 @@ export type InquirySaveResponse = { qnaNo: number //qna number mailYn: string //mail yn - Y / N } + +export type CommonCode = { + headCd: string + code: string + name: string + refChar1: string +} From 57a1e6a69b09949533ab109a8aa35398df9b80ec Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Fri, 23 May 2025 10:38:35 +0900 Subject: [PATCH 10/19] fix: inquiryList reactQuery delete return placeholderData --- .env.localhost | 2 +- src/components/inquiry/list/ListTable.tsx | 11 ++++------- src/hooks/useInquiry.ts | 4 ++-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.env.localhost b/.env.localhost index 966e366..6224cae 100644 --- a/.env.localhost +++ b/.env.localhost @@ -8,7 +8,7 @@ NEXT_PUBLIC_API_URL=http://localhost:3000 NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 #1:1문의 api -NEXT_PUBLIC_INQUIRY_API_URL=http://1.248.227.176:38080 +NEXT_PUBLIC_INQUIRY_API_URL=http://172.23.4.129:8110 #QPARTNER 로그인 api DB_HOST=202.218.61.226 diff --git a/src/components/inquiry/list/ListTable.tsx b/src/components/inquiry/list/ListTable.tsx index f7d874b..8d7878e 100644 --- a/src/components/inquiry/list/ListTable.tsx +++ b/src/components/inquiry/list/ListTable.tsx @@ -25,7 +25,7 @@ export default function ListTable() { const router = useRouter() const pathname = usePathname() - const { inquiryList } = useInquiry() + const { inquiryList, isLoadingInquiryList } = useInquiry() const { inquiryListRequest, setInquiryListRequest, reset } = useInquiryFilterStore() const [offset, setOffset] = useState(inquiryListRequest.startRow) @@ -34,7 +34,6 @@ export default function ListTable() { const [heldInquiryList, setHeldInquiryList] = useState([]) const { session } = useSessionStore() - useEffect(() => { setInquiryListRequest({ ...inquiryListRequest, startRow: 1, endRow: 10 }) setHeldInquiryList([]) @@ -43,15 +42,13 @@ export default function ListTable() { }, [pathname]) useEffect(() => { - if (!session.isLoggedIn || !inquiryList) return + if (!session.isLoggedIn || isLoadingInquiryList) return if (session.isLoggedIn) { setInquiryListRequest({ ...inquiryListRequest, storeId: session.storeId ?? '', loginId: session.userId ?? '' }) } if (inquiryList.length > 0 && inquiryList[0].totCnt > 0) { if (inquiryListRequest.startRow > 1) { - const isDuplicate = inquiryList.every((newItem) => heldInquiryList.some((existingItem) => existingItem.qnaNo === newItem.qnaNo)) - if (isDuplicate) return - setHeldInquiryList((prev) => [...prev, ...inquiryList]) + setHeldInquiryList([...heldInquiryList, ...inquiryList]) } else { setHeldInquiryList(inquiryList) } @@ -60,7 +57,7 @@ export default function ListTable() { setHeldInquiryList([]) setHasMore(false) } - }, [session, inquiryList, inquiryListRequest.startRow]) + }, [session, inquiryList, inquiryListRequest.startRow, isLoadingInquiryList]) const handleMyInquiry = () => { setInquiryListRequest({ ...inquiryListRequest, schRegId: inquiryListRequest.schRegId ? null : session.userId }) diff --git a/src/hooks/useInquiry.ts b/src/hooks/useInquiry.ts index 3806ec1..6575613 100644 --- a/src/hooks/useInquiry.ts +++ b/src/hooks/useInquiry.ts @@ -36,12 +36,12 @@ export function useInquiry( return [] } }, - placeholderData: (previousData) => previousData, + enabled: !!inquiryListRequest, }) const inquriyListData = useMemo(() => { if (isLoadingInquiryList) { - return { inquiryList: inquiryList ?? [] } + return { inquiryList: [] } } return { inquiryList: inquiryList ?? [], From 991cb5f0b2b61228320ca71f3e2d1b11403a1999 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Fri, 23 May 2025 13:23:12 +0900 Subject: [PATCH 11/19] fix: solve inquiry list startRow and endRow parameters not initializing --- src/components/inquiry/list/ListForm.tsx | 5 ++++- src/components/inquiry/list/ListTable.tsx | 17 +++++++---------- src/hooks/useInquiry.ts | 6 +++--- src/store/inquiryFilterStore.ts | 9 +++++---- src/types/Inquiry.ts | 2 -- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/components/inquiry/list/ListForm.tsx b/src/components/inquiry/list/ListForm.tsx index bb41fb3..badcbcd 100644 --- a/src/components/inquiry/list/ListForm.tsx +++ b/src/components/inquiry/list/ListForm.tsx @@ -5,12 +5,15 @@ import { useState } from 'react' export default function ListForm() { const router = useRouter() - const { inquiryListRequest, setInquiryListRequest } = useInquiryFilterStore() + const { inquiryListRequest, setInquiryListRequest, reset } = useInquiryFilterStore() const [searchKeyword, setSearchKeyword] = useState(inquiryListRequest.schTitle ?? '') const handleSearch = () => { if (searchKeyword.length >= 2) { + reset() setInquiryListRequest({ ...inquiryListRequest, schTitle: searchKeyword }) + } else { + alert('2文字以上入力してください') } } const handleKeyDown = (e: React.KeyboardEvent) => { diff --git a/src/components/inquiry/list/ListTable.tsx b/src/components/inquiry/list/ListTable.tsx index 8d7878e..d01de9d 100644 --- a/src/components/inquiry/list/ListTable.tsx +++ b/src/components/inquiry/list/ListTable.tsx @@ -26,19 +26,17 @@ export default function ListTable() { const pathname = usePathname() const { inquiryList, isLoadingInquiryList } = useInquiry() - const { inquiryListRequest, setInquiryListRequest, reset } = useInquiryFilterStore() + const { inquiryListRequest, setInquiryListRequest, reset, offset, setOffset } = useInquiryFilterStore() - const [offset, setOffset] = useState(inquiryListRequest.startRow) const [hasMore, setHasMore] = useState(false) const [heldInquiryList, setHeldInquiryList] = useState([]) const { session } = useSessionStore() + useEffect(() => { - setInquiryListRequest({ ...inquiryListRequest, startRow: 1, endRow: 10 }) - setHeldInquiryList([]) setOffset(1) - setHasMore(false) + setHeldInquiryList([]) }, [pathname]) useEffect(() => { @@ -47,17 +45,17 @@ export default function ListTable() { setInquiryListRequest({ ...inquiryListRequest, storeId: session.storeId ?? '', loginId: session.userId ?? '' }) } if (inquiryList.length > 0 && inquiryList[0].totCnt > 0) { - if (inquiryListRequest.startRow > 1) { + if (offset > 1) { setHeldInquiryList([...heldInquiryList, ...inquiryList]) } else { setHeldInquiryList(inquiryList) } - setHasMore(inquiryList[0].totCnt > inquiryListRequest.endRow) + setHasMore(inquiryList[0].totCnt > offset + 9) } else { setHeldInquiryList([]) setHasMore(false) } - }, [session, inquiryList, inquiryListRequest.startRow, isLoadingInquiryList]) + }, [session, inquiryList]) const handleMyInquiry = () => { setInquiryListRequest({ ...inquiryListRequest, schRegId: inquiryListRequest.schRegId ? null : session.userId }) @@ -129,8 +127,7 @@ export default function ListTable() { { - setInquiryListRequest({ ...inquiryListRequest, startRow: offset + 10, endRow: offset + 19 }) - setOffset(inquiryListRequest.startRow) + setOffset(offset + 10) }} /> diff --git a/src/hooks/useInquiry.ts b/src/hooks/useInquiry.ts index 6575613..11c750a 100644 --- a/src/hooks/useInquiry.ts +++ b/src/hooks/useInquiry.ts @@ -19,16 +19,16 @@ export function useInquiry( commonCodeList: CommonCode[] } { const queryClient = useQueryClient() - const { inquiryListRequest } = useInquiryFilterStore() + const { inquiryListRequest, offset } = useInquiryFilterStore() const { session } = useSessionStore() const { axiosInstance } = useAxios() const { data: inquiryList, isLoading: isLoadingInquiryList } = useQuery({ - queryKey: ['inquiryList', inquiryListRequest], + queryKey: ['inquiryList', inquiryListRequest, offset], queryFn: async () => { try { const resp = await axiosInstance(null).get<{ data: InquiryList[] }>(`/api/qna/list`, { - params: { inquiryListRequest }, + params: { inquiryListRequest, startRow: offset, endRow: offset + 9 }, }) return resp.data.data } catch (error: any) { diff --git a/src/store/inquiryFilterStore.ts b/src/store/inquiryFilterStore.ts index 62f70df..b3fb8d9 100644 --- a/src/store/inquiryFilterStore.ts +++ b/src/store/inquiryFilterStore.ts @@ -5,6 +5,8 @@ type InquiryFilterState = { inquiryListRequest: InquiryListRequest setInquiryListRequest: (inquiryListRequest: InquiryListRequest) => void reset: () => void + offset: number + setOffset: (offset: number) => void } export const useInquiryFilterStore = create((set) => ({ @@ -18,8 +20,6 @@ export const useInquiryFilterStore = create((set) => ({ schFromDt: null, schToDt: null, schAnswerYn: null, - startRow: 1, - endRow: 10, loginId: '', }, setInquiryListRequest: (inquiryListRequest) => set({ inquiryListRequest }), @@ -35,9 +35,10 @@ export const useInquiryFilterStore = create((set) => ({ schFromDt: '', schToDt: '', schAnswerYn: null, - startRow: 1, - endRow: 10, loginId: '', }, + offset: 1, }), + offset: 1, + setOffset: (offset) => set({ offset }), })) diff --git a/src/types/Inquiry.ts b/src/types/Inquiry.ts index 475ed79..c30a5ce 100644 --- a/src/types/Inquiry.ts +++ b/src/types/Inquiry.ts @@ -8,8 +8,6 @@ export type InquiryListRequest = { schFromDt: string | null //search start date schToDt: string | null //search end date schAnswerYn: string | null //search answer yn - startRow: number //start row - endRow: number //end row loginId: string //login id } From ff099f348e15b1390fcd35d6adf4efbd6a3d1844 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Mon, 26 May 2025 13:33:16 +0900 Subject: [PATCH 12/19] fix: change InquiryList type column --- src/components/inquiry/Detail.tsx | 2 +- src/types/Inquiry.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/inquiry/Detail.tsx b/src/components/inquiry/Detail.tsx index 29482d4..f8942fd 100644 --- a/src/components/inquiry/Detail.tsx +++ b/src/components/inquiry/Detail.tsx @@ -33,7 +33,7 @@ export default function Detail() { 作者 - {inquiryDetail?.regNm} + {inquiryDetail?.regUserNm} 販売店 diff --git a/src/types/Inquiry.ts b/src/types/Inquiry.ts index c30a5ce..98a98c3 100644 --- a/src/types/Inquiry.ts +++ b/src/types/Inquiry.ts @@ -25,6 +25,7 @@ export type InquiryList = { qnaClsLrgCd: string //qna CLS large Code qnaClsMidCd: string //qna CLS Mid Code qnaClsSmlCd: string | null //qna CLS Small Code + regUserNm: string //registration User name } export type InquiryDetailRequest = { From 1adbb24daa3b54c8b383a05dc6dd754ab4854ec9 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Tue, 27 May 2025 15:30:54 +0900 Subject: [PATCH 13/19] feat: add commonCodeList data at detail page --- src/components/inquiry/Detail.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/inquiry/Detail.tsx b/src/components/inquiry/Detail.tsx index f8942fd..76ffaad 100644 --- a/src/components/inquiry/Detail.tsx +++ b/src/components/inquiry/Detail.tsx @@ -9,6 +9,7 @@ export default function Detail() { const id = params.id const { inquiryDetail, downloadFile } = useInquiry(Number(id), '5200') + const { commonCodeList } = useInquiry() const router = useRouter() return ( @@ -52,9 +53,9 @@ export default function Detail() {
    - {inquiryDetail?.qnaClsLrgCd} - {inquiryDetail?.qnaClsMidCd} - {inquiryDetail?.qnaClsSmlCd} + {commonCodeList.find((code) => code.code === inquiryDetail?.qnaClsLrgCd)?.name} + {commonCodeList.find((code) => code.code === inquiryDetail?.qnaClsMidCd)?.name} + {commonCodeList.find((code) => code.code === inquiryDetail?.qnaClsSmlCd)?.name}
    {inquiryDetail?.qstTitle}
    {inquiryDetail?.qstContents}
    From 52a2032a8c5061c3a6590b9f1fcefd3779d1d571 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Wed, 28 May 2025 11:25:30 +0900 Subject: [PATCH 14/19] feat: implement file download function --- .env.development | 2 ++ .env.localhost | 1 + src/app/api/qna/file/route.ts | 17 ++++++++++++++--- src/app/api/qna/save/route.ts | 1 + src/components/inquiry/Answer.tsx | 10 ++++++++-- src/components/inquiry/Detail.tsx | 7 ++++++- src/components/inquiry/RegistForm.tsx | 19 +++++++++---------- src/hooks/useInquiry.ts | 22 ++++++++++++++++++---- 8 files changed, 59 insertions(+), 20 deletions(-) diff --git a/.env.development b/.env.development index 8e6fead..b446848 100644 --- a/.env.development +++ b/.env.development @@ -9,6 +9,8 @@ NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 #1:1문의 api NEXT_PUBLIC_INQUIRY_API_URL=http://172.23.4.129:8110 +NEXT_PUBLIC_INQUIRY_FILE_DOWNLOAD_API_URL=https://jp-dev.qsalesplatform.com + #QPARTNER 로그인 api DB_HOST=202.218.61.226 diff --git a/.env.localhost b/.env.localhost index 29cb5c2..ba9def8 100644 --- a/.env.localhost +++ b/.env.localhost @@ -9,6 +9,7 @@ NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 #1:1문의 api NEXT_PUBLIC_INQUIRY_API_URL=http://172.23.4.129:8110 +NEXT_PUBLIC_INQUIRY_FILE_DOWNLOAD_API_URL=https://jp-dev.qsalesplatform.com #QPARTNER 로그인 api DB_HOST=202.218.61.226 diff --git a/src/app/api/qna/file/route.ts b/src/app/api/qna/file/route.ts index a122da6..38aaf1b 100644 --- a/src/app/api/qna/file/route.ts +++ b/src/app/api/qna/file/route.ts @@ -4,19 +4,30 @@ import { NextResponse } from 'next/server' export async function GET(request: Request) { const { searchParams } = new URL(request.url) const encodeFileNo = searchParams.get('encodeFileNo') + const srcFileNm = searchParams.get('srcFileNm') if (!encodeFileNo) { return NextResponse.json({ error: 'encodeFileNo is required' }, { status: 400 }) } try { - const response = await axios.get(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/file/downloadFile`, { + const response = await axios.get(`${process.env.NEXT_PUBLIC_INQUIRY_FILE_DOWNLOAD_API_URL}/api/file/downloadFile2`, { + responseType: 'arraybuffer', params: { encodeFileNo, }, }) - return NextResponse.json(response.data) + if (response.headers['content-type'] === 'application/octet-stream;charset=UTF-8') { + return new NextResponse(response.data, { + status: 200, + headers: { + 'Content-Type': 'application/octet-stream;charset=UTF-8', + 'Content-Disposition': `attachment; filename="${srcFileNm}"`, + }, + }) + } else { + return NextResponse.json({ error: 'file not found' }, { status: 404 }) + } } catch (error: any) { - console.error(error.response) return NextResponse.json({ error: error.response.data }, { status: 500 }) } } diff --git a/src/app/api/qna/save/route.ts b/src/app/api/qna/save/route.ts index f51ae66..b440ef8 100644 --- a/src/app/api/qna/save/route.ts +++ b/src/app/api/qna/save/route.ts @@ -3,6 +3,7 @@ import { NextResponse } from 'next/server' export async function POST(request: Request) { const formData = await request.formData() + console.log(formData) try { const response = await axios.post(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/qna/save`, formData, { headers: { diff --git a/src/components/inquiry/Answer.tsx b/src/components/inquiry/Answer.tsx index 76da68a..4df9c3b 100644 --- a/src/components/inquiry/Answer.tsx +++ b/src/components/inquiry/Answer.tsx @@ -2,7 +2,13 @@ import { Inquiry } from '@/types/Inquiry' -export default function Answer({ inquiryDetail, downloadFile }: { inquiryDetail: Inquiry; downloadFile: (encodeFileNo: number) => Promise }) { +export default function Answer({ + inquiryDetail, + downloadFile, +}: { + inquiryDetail: Inquiry + downloadFile: (encodeFileNo: number, srcFileNm: string) => Promise +}) { return ( <>
    @@ -21,7 +27,7 @@ export default function Answer({ inquiryDetail, downloadFile }: { inquiryDetail:
      {inquiryDetail?.ansListFile?.map((file) => (
    • -
    • diff --git a/src/components/inquiry/Detail.tsx b/src/components/inquiry/Detail.tsx index 76ffaad..9e5d971 100644 --- a/src/components/inquiry/Detail.tsx +++ b/src/components/inquiry/Detail.tsx @@ -65,7 +65,12 @@ export default function Detail() {
        {inquiryDetail?.listFile?.map((file) => (
      • -
      • diff --git a/src/components/inquiry/RegistForm.tsx b/src/components/inquiry/RegistForm.tsx index 86d2ec9..0c3f452 100644 --- a/src/components/inquiry/RegistForm.tsx +++ b/src/components/inquiry/RegistForm.tsx @@ -74,7 +74,6 @@ export default function RegistForm() { focusOnRequiredField('qstMail') return } - const formData = new FormData() attachedFiles.forEach((file) => { formData.append('files', file) @@ -82,12 +81,6 @@ export default function RegistForm() { Object.entries(inquiryRequest).forEach(([key, value]) => { formData.append(key, value ?? '') }) - - const formDataObj: Record = {} - formData.forEach((value, key) => { - formDataObj[key] = value - }) - window.neoConfirm( 'お問い合わせを登録しますか? Hanwha Japanの担当者にお問い合わせメールが送信されます。', async () => { @@ -115,7 +108,9 @@ export default function RegistForm() { value={inquiryRequest.qnaClsLrgCd} onChange={(e) => setInquiryRequest({ ...inquiryRequest, qnaClsLrgCd: e.target.value })} > - + {commonCodeList .filter((code) => code.headCd === '204200') .map((code) => ( @@ -133,7 +128,9 @@ export default function RegistForm() { value={inquiryRequest.qnaClsMidCd} onChange={(e) => setInquiryRequest({ ...inquiryRequest, qnaClsMidCd: e.target.value })} > - + {commonCodeList .filter((code) => code.refChar1 === inquiryRequest.qnaClsLrgCd) .map((code) => ( @@ -151,7 +148,9 @@ export default function RegistForm() { value={inquiryRequest.qnaClsSmlCd ?? ''} onChange={(e) => setInquiryRequest({ ...inquiryRequest, qnaClsSmlCd: e.target.value })} > - + {commonCodeList .filter((code) => code.refChar1 === inquiryRequest.qnaClsMidCd) .map((code) => ( diff --git a/src/hooks/useInquiry.ts b/src/hooks/useInquiry.ts index 11c750a..a5ed2ab 100644 --- a/src/hooks/useInquiry.ts +++ b/src/hooks/useInquiry.ts @@ -15,7 +15,7 @@ export function useInquiry( isLoadingInquiryDetail: boolean isSavingInquiry: boolean saveInquiry: (formData: FormData) => Promise - downloadFile: (encodeFileNo: number) => Promise + downloadFile: (encodeFileNo: number, srcFileNm: string) => Promise commonCodeList: CommonCode[] } { const queryClient = useQueryClient() @@ -74,9 +74,23 @@ export function useInquiry( }, }) - const downloadFile = async (encodeFileNo: number) => { - const resp = await axiosInstance(null).get(`/api/qna/file`, { params: { encodeFileNo } }) - return resp.data + const downloadFile = async (encodeFileNo: number, srcFileNm: string) => { + try { + const resp = await axiosInstance(null).get(`/api/qna/file`, { params: { encodeFileNo, srcFileNm } }) + const blob = new Blob([resp.data], { type: 'application/octet-stream;charset=UTF-8' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `${srcFileNm}` + a.click() + URL.revokeObjectURL(url) + return blob + } catch (error: any) { + if (error.response.status === 404) { + alert('ファイルが見つかりません') + } + return null + } } const { data: commonCodeList, isLoading: isLoadingCommonCodeList } = useQuery({ From 5be7237aa634ecd204d90f3ebe18f00e7180555c Mon Sep 17 00:00:00 2001 From: Daseul Kim Date: Thu, 29 May 2025 10:07:05 +0900 Subject: [PATCH 15/19] =?UTF-8?q?feat:=20=EC=A7=80=EB=B6=95=EC=9E=AC?= =?UTF-8?q?=EC=A0=81=ED=95=A9=EC=84=B1=20=EC=83=81=EC=84=B8=ED=8C=9D?= =?UTF-8?q?=EC=97=85=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=91=9C=EC=B6=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20TODO=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 24 +-- src/app/api/suitable/route.ts | 30 ++-- src/components/popup/SuitableDetailPopup.tsx | 141 ++++++------------ .../popup/SuitableDetailPopupButton.tsx | 16 +- src/components/suitable/SuitableList.tsx | 32 ++-- src/hooks/useSuitable.ts | 36 ++++- 6 files changed, 132 insertions(+), 147 deletions(-) diff --git a/README.md b/README.md index 6d332e0..c6869cd 100644 --- a/README.md +++ b/README.md @@ -64,15 +64,21 @@ session에 있는 role 키로 구분한다 # 지붕재 적합성 TODO ``` -const suitableCheck = (value: string) => { - if (value === '×') { - return - } else if (value === 'ー') { - return - } else { - return - } +const suitableCheckIcon = (value: string): string => { + const iconMap: Record = { + '×': '/assets/images/sub/compliance_x_icon.svg', + 'ー': '/assets/images/sub/compliance_quest_icon.svg', + default: '/assets/images/sub/compliance_check_icon.svg', } + return iconMap[value] || iconMap.default +} +const suitableCheckMemo = (value: string): string => { + if (value === '○') return '設置可' + if (value === '×') return '設置不可' + if (value === 'ー') return 'お問い合わせください' + return `${value}で設置可` +} ``` -- 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요 +- src/hooks/useSuitable.ts > suitableCheckIcon(), suitableCheckMemo() + - 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요 diff --git a/src/app/api/suitable/route.ts b/src/app/api/suitable/route.ts index df42e1e..173b6ea 100644 --- a/src/app/api/suitable/route.ts +++ b/src/app/api/suitable/route.ts @@ -2,12 +2,15 @@ import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/libs/prisma' import { Suitable } from '@/types/Suitable' -export async function GET(request: NextRequest) { +export async function POST(request: NextRequest) { try { - const searchParams = request.nextUrl.searchParams + const body: Record = await request.json() + const ids = body.ids + const detailIds = body.detailIds - const ids = searchParams.get('ids') - const detailIds = searchParams.get('subIds') + if (ids === '' || detailIds === '') { + return NextResponse.json({ error: '필수 파라미터가 누락되었습니다' }, { status: 400 }) + } let query = ` SELECT @@ -29,28 +32,22 @@ export async function GET(request: NextRequest) { , msd_json.memo FROM ms_suitable_detail msd_json WHERE msd.main_id = msd_json.main_id + AND msd_json.id IN (:detailIds) FOR JSON PATH ) AS detail FROM ms_suitable_detail msd GROUP BY msd.main_id ) AS details ON msm.id = details.main_id - --ids AND details.main_id IN (:mainIds) - --detailIds AND details.id IN (:detailIds) - WHERE 1=1 - --ids AND msm.id IN (:mainIds) + AND details.main_id IN (:mainIds) + WHERE + msm.id IN (:mainIds) ORDER BY msm.product_name; ` // 검색 조건 설정 - if (ids) { - query = query.replaceAll('--ids ', '') - query = query.replaceAll(':mainIds', ids) - if (detailIds) { - query = query.replaceAll('--detailIds ', '') - query = query.replaceAll(':detailIds', detailIds) - } - } + query = query.replaceAll(':mainIds', ids) + query = query.replaceAll(':detailIds', detailIds) const suitable: Suitable[] = await prisma.$queryRawUnsafe(query) @@ -60,4 +57,3 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) } } - diff --git a/src/components/popup/SuitableDetailPopup.tsx b/src/components/popup/SuitableDetailPopup.tsx index 80524d0..4baf4ec 100644 --- a/src/components/popup/SuitableDetailPopup.tsx +++ b/src/components/popup/SuitableDetailPopup.tsx @@ -3,15 +3,13 @@ import Image from 'next/image' import { useCallback, useEffect, useState } from 'react' import { usePopupController } from '@/store/popupController' -import { useSuitableStore } from '@/store/useSuitableStore' import SuitableDetailPopupButton from './SuitableDetailPopupButton' import { useSuitable } from '@/hooks/useSuitable' -import { Suitable } from '@/types/Suitable' +import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/Suitable' export default function SuitableDetailPopup() { const popupController = usePopupController() - const { getSuitableDetails, serializeSelectedItems } = useSuitable() - const { selectedItems } = useSuitableStore() + const { getSelectedItemsData, toCodeName, toSuitableDetail, suitableCheckIcon, suitableCheckMemo } = useSuitable() const [openItems, setOpenItems] = useState>(new Set()) const [suitableDetails, setSuitableDetails] = useState([]) @@ -25,14 +23,9 @@ export default function SuitableDetailPopup() { }) }, []) - // 선택된 아이템 상세 데이터 가져오기 - const getSelectedItemsData = async () => { - const serialized: Map = serializeSelectedItems() - setSuitableDetails(await getSuitableDetails(serialized.get('ids') ?? '', serialized.get('detailIds') ?? '')) - } - useEffect(() => { - getSelectedItemsData() + // TODO: 로딩 처리 필요 + getSelectedItemsData().then((data) => setSuitableDetails(data)) }, []) return ( @@ -52,98 +45,56 @@ export default function SuitableDetailPopup() {
    -
    -
    -
    アースティ40
    -
    - -
    -
    -
    -
    -
    屋根技研 支持瓦
    -
    ㈱ダイトー
    -
    -
    -
    屋根材
    -
    -
    -
    -
    金具タイプ
    -
    木ねじ打ち込み式
    -
    -
    -
    -
    -
    屋根技研 支持瓦
    -
    -
    - -
    -
    - -
    -
    -
    -
    Dで設置可
    -
    -
    備考
    -
    - 桟木なしの場合は支持金具平ー1で設置可能。その場合水返しが高い為、レベルプレート使用。桟木ありの場合は支持金具平ー2で設置可能 -
    -
    + {suitableDetails.map((item: Suitable) => ( +
    +
    +
    {item.productName}
    +
    +
    -
    -
    -
    屋根技研支持金具
    -
    -
    - -
    -
    - -
    -
    -
    -
    設置不可
    -
    -
    備考
    -
    入手困難
    -
    +
    +
    +
    +
    屋根技研 支持瓦
    +
    {toCodeName(SUITABLE_HEAD_CODE.MANU_FT_CD, item.manuFtCd)}
    -
    -
    -
    屋根技研YGアンカー
    -
    -
    - -
    -
    -
    -
    お問い合わせください
    -
    -
    備考
    -
    入手困難
    -
    +
    +
    屋根材
    +
    {toCodeName(SUITABLE_HEAD_CODE.ROOF_MT_CD, item.roofMtCd)}
    -
    -
    -
    ダイドーハント支持瓦Ⅱ
    -
    -
    - +
    +
    金具タイプ
    +
    {toCodeName(SUITABLE_HEAD_CODE.ROOF_SH_CD, item.roofShCd)}
    +
    +
    + {toSuitableDetail(item.detail).map((subItem: SuitableDetail) => ( +
    +
    +
    {toCodeName(SUITABLE_HEAD_CODE.TRESTLE_MFPC_CD, subItem.trestleMfpcCd)}
    +
    +
    + +
    + {subItem.memo && ( +
    + +
    + )} +
    +
    {suitableCheckMemo(subItem.trestleManufacturerProductName)}
    + {subItem.memo && ( +
    +
    備考
    +
    {subItem.memo}
    +
    + )}
    -
    -
    Ⅳ (D) で設置可
    -
    -
    備考
    -
    入手困難
    -
    + ))}
    -
    + ))}
    diff --git a/src/components/popup/SuitableDetailPopupButton.tsx b/src/components/popup/SuitableDetailPopupButton.tsx index 52e1bae..01f88ce 100644 --- a/src/components/popup/SuitableDetailPopupButton.tsx +++ b/src/components/popup/SuitableDetailPopupButton.tsx @@ -1,10 +1,16 @@ 'use client' +import { useRouter } from 'next/navigation' +import { usePopupController } from '@/store/popupController' + export default function SuitableDetailPopupButton() { + const popupController = usePopupController() + const router = useRouter() + return (
    -
    @@ -14,7 +20,13 @@ export default function SuitableDetailPopupButton() {
    -
    diff --git a/src/components/suitable/SuitableList.tsx b/src/components/suitable/SuitableList.tsx index 9936494..7aaeb5e 100644 --- a/src/components/suitable/SuitableList.tsx +++ b/src/components/suitable/SuitableList.tsx @@ -9,7 +9,17 @@ import { useSuitableStore } from '@/store/useSuitableStore' import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/Suitable' export default function SuitableList() { - const { toCodeName, toSuitableDetail, toSuitableDetailIds, suitables, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useSuitable() + const { + toCodeName, + toSuitableDetail, + toSuitableDetailIds, + suitables, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + suitableCheckIcon, + } = useSuitable() const { selectedItems, addSelectedItem, removeSelectedItem } = useSuitableStore() const [openItems, setOpenItems] = useState>(new Set()) const observerTarget = useRef(null) @@ -52,20 +62,6 @@ export default function SuitableList() { }) }, []) - // TODO: 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요 - const suitableCheck = useCallback((value: string) => { - const iconMap: Record = { - '×': '/assets/images/sub/compliance_x_icon.svg', - ー: '/assets/images/sub/compliance_quest_icon.svg', - default: '/assets/images/sub/compliance_check_icon.svg', - } - return ( -
    - -
    - ) - }, []) - // 아이템 렌더링 const renderItem = useCallback( (item: Suitable) => { @@ -99,7 +95,9 @@ export default function SuitableList() {
    - {suitableCheck(subItem.trestleManufacturerProductName)} +
    + +
    {subItem.memo && (
    @@ -113,7 +111,7 @@ export default function SuitableList() {
    ) }, - [isItemSelected, openItems, handleItemClick, toggleItemOpen, suitableCheck, toCodeName, toSuitableDetail], + [isItemSelected, openItems, handleItemClick, toggleItemOpen, toCodeName, toSuitableDetail], ) // 아이템 리스트 diff --git a/src/hooks/useSuitable.ts b/src/hooks/useSuitable.ts index 5acad45..23b89b5 100644 --- a/src/hooks/useSuitable.ts +++ b/src/hooks/useSuitable.ts @@ -62,7 +62,7 @@ export function useSuitable() { try { const params: Record = { ids: ids } if (detailIds) params.detailIds = detailIds - const response = await axiosInstance(null).get('/api/suitable', { params }) + const response = await axiosInstance(null).post('/api/suitable', params) return response.data } catch (error) { console.error('지붕재 상세 데이터 로드 실패:', error) @@ -134,17 +134,19 @@ export function useSuitable() { enabled: selectedCategory !== '' || searchKeyword !== '', }) - const serializeSelectedItems = (): Map => { + const serializeSelectedItems = (): { ids: string; detailIds: string } => { const ids: string[] = [] const detailIds: string[] = [] for (const [key, value] of selectedItems) { ids.push(String(key)) for (const id of value) detailIds.push(String(id)) } - return new Map([ - ['ids', ids.join(',')], - ['detailIds', detailIds.join(',')], - ]) + return { ids: ids.join(','), detailIds: detailIds.length > 0 ? detailIds.join(',') : '' } + } + + const getSelectedItemsData = async (): Promise => { + const { ids, detailIds } = serializeSelectedItems() + return await getSuitableDetails(ids, detailIds) } const clearSuitableSearch = ({ items = false, category = false, keyword = false }: { items?: boolean; category?: boolean; keyword?: boolean }) => { @@ -153,6 +155,24 @@ export function useSuitable() { if (keyword) clearSearchKeyword() } + // TODO: 추후 지붕재 적합성 데이터 CUD 구현 시 ×, ー 데이터 관리 필요 + const suitableCheckIcon = (value: string): string => { + const iconMap: Record = { + '×': '/assets/images/sub/compliance_x_icon.svg', + 'ー': '/assets/images/sub/compliance_quest_icon.svg', + default: '/assets/images/sub/compliance_check_icon.svg', + } + return iconMap[value] || iconMap.default + } + + // TODO: 추후 지붕재 적합성 데이터 CUD 구현 시 ○, ×, ー 데이터 관리 필요 + const suitableCheckMemo = (value: string): string => { + if (value === '○') return '設置可' + if (value === '×') return '設置不可' + if (value === 'ー') return 'お問い合わせください' + return `${value}で設置可` + } + return { getSuitables, getSuitableIds, @@ -166,7 +186,9 @@ export function useSuitable() { hasNextPage, isFetchingNextPage, isLoading, - serializeSelectedItems, + getSelectedItemsData, clearSuitableSearch, + suitableCheckIcon, + suitableCheckMemo, } } From f1617045a5147ad4801a5471f2ecd0bd6e785273 Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Thu, 29 May 2025 10:23:35 +0900 Subject: [PATCH 16/19] chore: update environment files to use localhost for API URLs and add nodemailer types to package-lock --- .env.development | 2 +- .env.localhost | 2 +- .env.production | 2 +- package-lock.json | 10 + src/components/pdf/SuitableDownloadPdf.tsx | 2066 ++++++-------------- src/config/config.local.ts | 2 +- src/styles/components/_pdfview.scss | 81 + src/styles/components/_pop-contents.scss | 9 + 8 files changed, 748 insertions(+), 1426 deletions(-) diff --git a/.env.development b/.env.development index 2034c56..205b2a4 100644 --- a/.env.development +++ b/.env.development @@ -2,7 +2,7 @@ NEXT_PUBLIC_RUN_MODE=development # 모바일 디바이스로 로컬 서버 확인하려면 자신 IP 주소로 변경 # 다시 로컬에서 개발할때는 localhost로 변경 #route handler -NEXT_PUBLIC_API_URL=http://172.30.1.23:3000 +NEXT_PUBLIC_API_URL=http://localhost:3000 #qsp 로그인 api NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 diff --git a/.env.localhost b/.env.localhost index 844bc76..5f61c6f 100644 --- a/.env.localhost +++ b/.env.localhost @@ -2,7 +2,7 @@ NEXT_PUBLIC_RUN_MODE=local # 모바일 디바이스로 로컬 서버 확인하려면 자신 IP 주소로 변경 # 다시 로컬에서 개발할때는 localhost로 변경 #route handler -NEXT_PUBLIC_API_URL=http://172.30.1.23:3000 +NEXT_PUBLIC_API_URL=http://localhost:3000 #qsp 로그인 api NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 diff --git a/.env.production b/.env.production index 72caa0d..fc11d33 100644 --- a/.env.production +++ b/.env.production @@ -1,6 +1,6 @@ NEXT_PUBLIC_RUN_MODE=production #route handler -NEXT_PUBLIC_API_URL=http://1.248.227.176:3000 +NEXT_PUBLIC_API_URL=http://localhost:3000 #qsp 로그인 api # NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 diff --git a/package-lock.json b/package-lock.json index 6a3c17f..669f7b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@prisma/client": "^6.7.0", "@tanstack/react-query": "^5.71.0", "@tanstack/react-query-devtools": "^5.71.0", + "@types/nodemailer": "^6.4.17", "axios": "^1.8.4", "env-cmd": "^10.1.0", "iron-session": "^8.0.4", @@ -1964,6 +1965,15 @@ "undici-types": "~6.19.2" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.17", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", + "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/raf": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", diff --git a/src/components/pdf/SuitableDownloadPdf.tsx b/src/components/pdf/SuitableDownloadPdf.tsx index 86f5c60..60ffc07 100644 --- a/src/components/pdf/SuitableDownloadPdf.tsx +++ b/src/components/pdf/SuitableDownloadPdf.tsx @@ -33,1431 +33,653 @@ export default function SuitableDownloadPdf() { } return ( <> - -
    -
    -
    -
    - ハンファジャパン株式会社 -
    -
    (瓦) 屋根材適合表
    -
    - 2025年4月30日 10:40 -
    +
    +
    +
    +
    ハンファジャパン株式会社
    +
    (瓦) 屋根材適合表
    +
    2025年4月30日 10:40
    -
    -

    - 本適合表は参考資料としてご使用下さい。 -

    -

    - 屋根材製品の形状・仕様はメーカーより変更される場合が御座います。 -

    -

    - 又、現場環境(働き、勾配、瓦桟木条件等)により本適合表と異なる適合結果となる場合が御座います。予めご了承下さい。 -

    -

    - 屋根材以外の設置条件(垂木、野地板等の設置基準)も必ずご確認下さい。 -

    +
    +

    本適合表は参考資料としてご使用下さい。

    +

    屋根材製品の形状・仕様はメーカーより変更される場合が御座います。

    +

    又、現場環境(働き、勾配、瓦桟木条件等)により本適合表と異なる適合結果となる場合が御座います。予めご了承下さい。

    +

    屋根材以外の設置条件(垂木、野地板等の設置基準)も必ずご確認下さい。

    -
    - - - - - - - - - - - - - - - - - - - - -
    -
    - - 屋根材製品名 - - - メーカー名 - - - 屋根材の種類 - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - 金具タイプ - - 金具名 - - 設置可否 - - 備考 -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    -
    -
    - - 屋根材製品名 - - - メーカー名 - - - 屋根材の種類 - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - 金具タイプ - - 金具名 - - 設置可否 - - 備考 -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    -
    -
    - - 屋根材製品名 - - - メーカー名 - - - 屋根材の種類 - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - 金具タイプ - - 金具名 - - 設置可否 - - 備考 -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    -
    -
    - - 屋根材製品名 - - - メーカー名 - - - 屋根材の種類 - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - 金具タイプ - - 金具名 - - 設置可否 - - 備考 -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    -
    -
    - - 屋根材製品名 - - - メーカー名 - - - 屋根材の種類 - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - 金具タイプ - - 金具名 - - 設置可否 - - 備考 -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    -
    -
    - - 屋根材製品名 - - - メーカー名 - - - 屋根材の種類 - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - 金具タイプ - - 金具名 - - 設置可否 - - 備考 -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    -
    -
    - - 屋根材製品名 - - - メーカー名 - - - 屋根材の種類 - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - 金具タイプ - - 金具名 - - 設置可否 - - 備考 -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    -
    -
    - - 屋根材製品名 - - - メーカー名 - - - 屋根材の種類 - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - 金具タイプ - - 金具名 - - 設置可否 - - 備考 -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    - 木ねじ打ち込み式 - - 屋根技研支持瓦 - - C で設置可 - - 支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身){' '} -
    -
    +
    +
    +
    +
    + 屋根材製品名 + メーカー名 + 屋根材の種類 +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    金具タイプ金具名設置可否備考
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    +
    +
    +
    +
    + 屋根材製品名 + メーカー名 + 屋根材の種類 +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    金具タイプ金具名設置可否備考
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    +
    +
    +
    +
    + 屋根材製品名 + メーカー名 + 屋根材の種類 +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    金具タイプ金具名設置可否備考
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    +
    +
    +
    +
    + 屋根材製品名 + メーカー名 + 屋根材の種類 +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    金具タイプ金具名設置可否備考
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    +
    +
    +
    +
    + 屋根材製品名 + メーカー名 + 屋根材の種類 +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    金具タイプ金具名設置可否備考
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    +
    +
    +
    +
    + 屋根材製品名 + メーカー名 + 屋根材の種類 +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    金具タイプ金具名設置可否備考
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    +
    +
    +
    +
    + 屋根材製品名 + メーカー名 + 屋根材の種類 +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    金具タイプ金具名設置可否備考
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    +
    +
    +
    +
    + 屋根材製品名 + メーカー名 + 屋根材の種類 +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    金具タイプ金具名設置可否備考
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    +
    +
    +
    +
    + 屋根材製品名 + メーカー名 + 屋根材の種類 +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    金具タイプ金具名設置可否備考
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    +
    +
    +
    +
    + 屋根材製品名 + メーカー名 + 屋根材の種類 +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    金具タイプ金具名設置可否備考
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    木ねじ打ち込み式屋根技研支持瓦C で設置可支持瓦はアンダーラップの先端を削って納める(Try-U40はこの瓦の前身)
    +
    +
    +
    diff --git a/src/config/config.local.ts b/src/config/config.local.ts index 8fbf68b..ac02c97 100644 --- a/src/config/config.local.ts +++ b/src/config/config.local.ts @@ -1,7 +1,7 @@ import getConfigs from '@/config/config.common' // 환경마다 달라져야 할 변수, 값들을 정의합니다. (여기는 local 환경에 맞는 값을 지정합니다.) -const baseUrl = 'http://172.30.1.23:3000' +const baseUrl = 'http://localhost:3000' const mode = 'local' // 환경마다 달라져야 할 값들을 getConfig 함수에 전달합니다. diff --git a/src/styles/components/_pdfview.scss b/src/styles/components/_pdfview.scss index 8db6cf7..039d1f5 100644 --- a/src/styles/components/_pdfview.scss +++ b/src/styles/components/_pdfview.scss @@ -1,5 +1,6 @@ @use "../abstracts" as *; +// 조사매물 .pdf-contents{ padding: 0 20px; border-top: 1px solid #ececec; @@ -54,4 +55,84 @@ @include defaultFont($font-s-11, $font-w-400, #FF5656); border: 1px solid $black-1010; min-height: 150px; +} + +// 지붕재 적합성 +.pdf-intro-page{ + height: 1080px; + padding: 80px 40px ; + background-color: #fff; +} +.pdf-intro-tit-wrap{ + text-align: center; + .pdf-intro-tit{ + @include defaultFont($font-s-24, $font-w-500, #101010); + } + .pdf-intro-date{ + @include defaultFont($font-s-22, $font-w-400, #101010); + } +} +.pdf-intro-cont-wrap{ + margin-top: 70px; + p{ + @include defaultFont($font-s-18, $font-w-400, #101010); + } +} + +.pdf-table-content{ + padding: 20px; +} +.pdf-table-grid-wrap{ + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px 20px; +} +.pdf-table-card{ + .pdf-table-tit-wrap{ + margin-bottom: 5px; + span{ + position: relative; + @include defaultFont($font-s-13, $font-w-500, #101010); + padding: 0 10px; + &:first-child{ + padding-left: 0; + } + &:last-child{ + padding-right: 0; + &::before{ + display: none; + } + } + &::before{ + content: ''; + position: absolute; + top: 50%; + right: 0; + transform: translateY(-50%); + width: 1px; + height: 14px; + background-color: #101010; + } + } + } +} +.pdf-roof-table{ + table{ + width: 100%; + table-layout: fixed; + border-collapse: collapse; + th{ + padding: 0px 5px; + text-align: center; + @include defaultFont($font-s-11, $font-w-500, #fff); + background-color: #18B490; + border: 1px solid #18B490; + } + td{ + padding: 0px 5px; + @include defaultFont($font-s-11, $font-w-300, #101010); + border: 1px solid #CBCBCB; + line-height: 1; + } + } } \ No newline at end of file diff --git a/src/styles/components/_pop-contents.scss b/src/styles/components/_pop-contents.scss index b598397..afa4b3a 100644 --- a/src/styles/components/_pop-contents.scss +++ b/src/styles/components/_pop-contents.scss @@ -112,4 +112,13 @@ padding: 10px; @include defaultFont($font-s-13, $font-w-400, $font-c); } +} + +// 제출팝업 +.submit-content{ + padding: 15px 10px; + border: 1px solid #D5DEE8; + border-radius: 4px; + background-color: #f5f6fa; + cursor: default; } \ No newline at end of file From 37435d2cca714bb9873553dd8577943b5f65cd7e Mon Sep 17 00:00:00 2001 From: yoosangwook Date: Thu, 29 May 2025 10:32:10 +0900 Subject: [PATCH 17/19] feat: add documentation for Login component, project structure, and login process - Introduced diagrams for the Login component structure, project architecture, and login sequence. - Detailed state management, external hooks, event handling, and UI components in the Login documentation. - Provided an overview of the component relationships and roles within the application architecture. --- diagram/Login.md | 88 +++++++++++++++++++++++++ diagram/mermaid.md | 98 +++++++++++++++++++++++++++ diagram/mermaid2.md | 125 +++++++++++++++++++++++++++++++++++ diagram/mermaid3.md | 157 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 468 insertions(+) create mode 100644 diagram/Login.md create mode 100644 diagram/mermaid.md create mode 100644 diagram/mermaid2.md create mode 100644 diagram/mermaid3.md diff --git a/diagram/Login.md b/diagram/Login.md new file mode 100644 index 0000000..2e816ca --- /dev/null +++ b/diagram/Login.md @@ -0,0 +1,88 @@ +# Login Component Structure + +## Component Diagram + +```mermaid +graph TD + A[Login Component] --> B[State Management] + B --> B1[useState Hooks] + B1 --> B1a[pwShow: 비밀번호 표시 여부] + B1 --> B1b[idSave: ID 저장 여부] + B1 --> B1c[isPartners: Q.PARTNERS 여부] + B1 --> B1d[isLogin: 로그인 상태] + + A --> C[Account Management] + C --> C1[useReducer] + C1 --> C1a[loginId] + C1 --> C1b[pwd] + + A --> D[External Hooks] + D --> D1[useRouter] + D --> D2[useLocalStorage] + D --> D3[useSessionStore] + D --> D4[useAxios] + D --> D5[useQuery] + + A --> E[Event Handlers] + E --> E1[handleLogin] + E --> E2[handleKeyDown] + E --> E3[validateLogin] + + A --> F[Effects] + F --> F1[Login Success Effect] + F1 --> F1a[세션 저장] + F1 --> F1b[라우팅] + F --> F2[Email Validation Effect] + F2 --> F2a[Partners 모드 전환] + + A --> G[UI Components] + G --> G1[Login Form] + G1 --> G1a[ID Input] + G1 --> G1b[Password Input] + G1 --> G1c[Checkboxes] + G1 --> G1d[Login Button] +``` + +## 주요 특징과 동작 방식 + +### 1. 상태 관리 + +- `useState`를 사용하여 UI 상태 관리 (비밀번호 표시, ID 저장, Partners 모드) +- `useReducer`를 사용하여 계정 정보(loginId, pwd) 관리 + +### 2. 외부 훅 통합 + +- `useRouter`: 페이지 라우팅 +- `useLocalStorage`: 로컬 스토리지 데이터 관리 +- `useSessionStore`: 세션 상태 관리 +- `useAxios`: API 통신 +- `useQuery`: 로그인 API 호출 및 상태 관리 + +### 3. 이벤트 처리 + +- `handleLogin`: 로그인 시도 +- `handleKeyDown`: Enter 키 입력 처리 +- `validateLogin`: 입력값 유효성 검사 + +### 4. 효과 처리 + +- 로그인 성공 시 세션 저장 및 라우팅 +- 이메일 형식에 따른 Partners 모드 자동 전환 + +### 5. UI 구성 + +- ID/PW 입력 필드 +- 비밀번호 표시/숨김 토글 +- ID 저장 체크박스 +- Q.PARTNERS 토글 +- 로그인 버튼 + +### 6. 보안 및 유효성 검사 + +- 이메일 형식 검증 +- 필수 입력값 검증 +- API 응답 코드에 따른 처리 (200: 성공, 400: 실패) + +## 특징 + +이 컴포넌트는 클라이언트 사이드에서 동작하며('use client'), 사용자 인증과 관련된 모든 로직을 포함하고 있습니다. 특히 Q.PARTNERS 모드와 일반 모드를 구분하여 다른 API 엔드포인트를 사용하는 특징이 있습니다. diff --git a/diagram/mermaid.md b/diagram/mermaid.md new file mode 100644 index 0000000..e6b5ef8 --- /dev/null +++ b/diagram/mermaid.md @@ -0,0 +1,98 @@ +# Project Structure Documentation + +## Component Relationship Diagram + +```mermaid +graph TD + subgraph Root + A[RootLayout] --> B[ReactQueryProviders] + B --> C[EdgeProvider] + C --> D[HTML Structure] + end + + subgraph Layout Components + D --> E[Header] + D --> F[Main Content] + D --> G[Footer] + D --> H[Float Button] + D --> I[PopupController] + end + + subgraph Pages + F --> J[Login] + F --> K[Survey Sale] + F --> L[Suitable] + F --> M[Inquiry] + F --> N[Password Reset] + F --> O[PDF] + end + + subgraph Providers + P1[ReactQueryProvider] + P2[EdgeProvider] + end + + subgraph Components + C1[UI Components] + C2[Popup Components] + C3[PDF Components] + C4[Survey Components] + C5[Inquiry Components] + end + + subgraph Utils + U1[Session Management] + U2[Mailer] + U3[API Routes] + end + + %% Relationships + A --> P1 + A --> P2 + J --> U1 + K --> C4 + L --> C4 + M --> C5 + N --> U2 + O --> C3 +``` + +## Structure Explanation + +### 1. Root Layout + +- `RootLayout`이 전체 애플리케이션의 기본 구조를 정의 +- `ReactQueryProviders`와 `EdgeProvider`로 감싸져 있음 + +### 2. Layout Components + +- Header, Footer, Float Button 등 공통 레이아웃 컴포넌트 +- `PopupController`로 팝업 관리 + +### 3. Pages + +- Next.js App Router 기반의 페이지 구조 +- Login, Survey Sale, Suitable, Inquiry 등 주요 페이지들 + +### 4. Providers + +- `ReactQueryProvider`: 데이터 페칭 관리 +- `EdgeProvider`: 세션 및 상태 관리 + +### 5. Components + +- UI Components: 공통 UI 요소 +- Popup Components: 팝업 관련 컴포넌트 +- PDF Components: PDF 생성/관리 컴포넌트 +- Survey Components: 설문 관련 컴포넌트 +- Inquiry Components: 문의 관련 컴포넌트 + +### 6. Utils + +- Session Management: 세션 관리 +- Mailer: 이메일 발송 기능 +- API Routes: 백엔드 API 엔드포인트 + +## Architecture Overview + +이 구조는 Next.js의 App Router를 기반으로 하며, 컴포넌트 기반 아키텍처를 따르고 있습니다. 각 기능별로 모듈화가 잘 되어있고, 공통 컴포넌트와 유틸리티를 효율적으로 재사용할 수 있도록 구성되어 있습니다. diff --git a/diagram/mermaid2.md b/diagram/mermaid2.md new file mode 100644 index 0000000..5d8e561 --- /dev/null +++ b/diagram/mermaid2.md @@ -0,0 +1,125 @@ +# Login Process Documentation + +## Login Sequence Diagram + +```mermaid +sequenceDiagram + actor User + participant Login as Login Component + participant API as Auth API + participant QSP as QSP API + participant Session as Session Store + participant Router as Next Router + + User->>Login: Enter credentials + Login->>Login: Validate input + alt Invalid Input + Login->>User: Show error message + else Valid Input + Login->>API: POST /api/auth + API->>QSP: POST /api/user/login + QSP-->>API: Return user data + + alt Login Success + API->>Session: Create session + Session->>Session: Set user data + Session->>Session: Set role + API-->>Login: Return success response + Login->>Router: Redirect to home + Router->>User: Show home page + else Login Failed + API-->>Login: Return error response + Login->>User: Show error message + end + end +``` + +## Login Process Flow + +1. **User Input** + + - User enters login credentials (ID and password) + - Optional: User can toggle Q.PARTNERS mode + - Optional: User can save ID + +2. **Input Validation** + + - Checks if ID and password are not empty + - Validates email format for Q.PARTNERS mode + +3. **Authentication Request** + + - Sends credentials to Auth API + - Auth API forwards request to QSP API + - QSP API validates credentials + +4. **Session Management** + + - On successful login: + - Creates new session + - Stores user data + - Sets user role based on permissions + - Saves session to cookies + +5. **Response Handling** + - Success: Redirects to home page + - Failure: Shows error message + +## Role Assignment Logic + +The system assigns roles based on the following rules: + +- `T01`: If userId is 'T01' +- `Admin`: If groupId is '60000' +- `Admin_Sub`: If groupId is '70000' and builderNo is null +- `Builder`: If groupId is '70000' and builderNo is not null +- `User`: Default role for all other cases + +```mermaid +graph TD +subgraph Root +A[RootLayout] --> B[ReactQueryProviders] +B --> C[EdgeProvider] +C --> D[HTML Structure] +end + subgraph Layout Components + D --> E[Header] + D --> F[Main Content] + D --> G[Footer] + D --> H[Float Button] + D --> I[PopupController] + end + subgraph Pages + F --> J[Login] + F --> K[Survey Sale] + F --> L[Suitable] + F --> M[Inquiry] + F --> N[Password Reset] + F --> O[PDF] + end + subgraph Providers + P1[ReactQueryProvider] + P2[EdgeProvider] + end + subgraph Components + C1[UI Components] + C2[Popup Components] + C3[PDF Components] + C4[Survey Components] + C5[Inquiry Components] + end + subgraph Utils + U1[Session Management] + U2[Mailer] + U3[API Routes] + end + %% Relationships + A --> P1 + A --> P2 + J --> U1 + K --> C4 + L --> C4 + M --> C5 + N --> U2 + O --> C3 +``` diff --git a/diagram/mermaid3.md b/diagram/mermaid3.md new file mode 100644 index 0000000..cf9fd15 --- /dev/null +++ b/diagram/mermaid3.md @@ -0,0 +1,157 @@ +# Pages and Components Class Diagram + +## Class Diagram + +```mermaid +classDiagram + class RootLayout { + +ReactNode children + +ReactNode header + +ReactNode footer + +ReactNode floatBtn + +ReactQueryProviders + +EdgeProvider + +PopupController + } + + class Page { + <> + +ReactNode render() + } + + class LoginPage { + +Login component + +handleLogin() + +validateInput() + } + + class SurveySalePage { + +SurveySaleList + +SurveySaleDetail + +handleSurveySubmit() + } + + class SuitablePage { + +SuitableList + +SuitableDetail + +handleSuitableSubmit() + } + + class InquiryPage { + +InquiryList + +InquiryDetail + +handleInquirySubmit() + } + + class PasswordResetPage { + +PasswordResetForm + +handlePasswordReset() + } + + class PDFPage { + +PDFViewer + +PDFGenerator + +handlePDFGeneration() + } + + class BaseComponent { + <> + +ReactNode render() + } + + class UIComponent { + +Button + +Input + +Select + +Modal + } + + class PopupComponent { + +PopupController + +PopupContent + +handlePopup() + } + + class PDFComponent { + +PDFViewer + +PDFGenerator + +handlePDF() + } + + class SurveyComponent { + +SurveyForm + +SurveyList + +handleSurvey() + } + + class InquiryComponent { + +InquiryForm + +InquiryList + +handleInquiry() + } + + %% Relationships + RootLayout --> Page + Page <|-- LoginPage + Page <|-- SurveySalePage + Page <|-- SuitablePage + Page <|-- InquiryPage + Page <|-- PasswordResetPage + Page <|-- PDFPage + + BaseComponent <|-- UIComponent + BaseComponent <|-- PopupComponent + BaseComponent <|-- PDFComponent + BaseComponent <|-- SurveyComponent + BaseComponent <|-- InquiryComponent + + LoginPage --> BaseComponent + SurveySalePage --> SurveyComponent + SuitablePage --> SurveyComponent + InquiryPage --> InquiryComponent + PasswordResetPage --> UIComponent + PDFPage --> PDFComponent + + RootLayout --> PopupComponent +``` + +## Component Hierarchy + +1. **Root Layout** + + - 최상위 레이아웃 컴포넌트 + - ReactQuery와 Edge Provider를 포함 + - 공통 레이아웃 요소 관리 + +2. **Pages** + + - LoginPage: 로그인 기능 + - SurveySalePage: 설문 판매 관리 + - SuitablePage: 적합성 관리 + - InquiryPage: 문의 관리 + - PasswordResetPage: 비밀번호 재설정 + - PDFPage: PDF 생성 및 관리 + +3. **Base Components** + - UIComponent: 기본 UI 요소 + - PopupComponent: 팝업 관리 + - PDFComponent: PDF 관련 기능 + - SurveyComponent: 설문 관련 기능 + - InquiryComponent: 문의 관련 기능 + +## Component Relationships + +1. **Page-Component Relationship** + + - 각 페이지는 필요한 컴포넌트들을 조합하여 구성 + - 페이지별로 특화된 컴포넌트 사용 + +2. **Component Inheritance** + + - 모든 컴포넌트는 BaseComponent 인터페이스 구현 + - 각 컴포넌트 타입별로 특화된 기능 제공 + +3. **Layout Integration** + - RootLayout이 전체 페이지 구조 관리 + - PopupComponent를 통한 모달 관리 + - 공통 UI 요소의 일관성 유지 From f0b23c35af754a2bdac14de2fc149d45fa1b1461 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Thu, 29 May 2025 11:35:19 +0900 Subject: [PATCH 18/19] feat: change the order of fields at inquiry regist, detail page --- .env.development | 5 +- .env.localhost | 5 +- src/app/api/qna/file/route.ts | 2 +- src/components/inquiry/Detail.tsx | 23 +- src/components/inquiry/ListTable.tsx | 91 ----- src/components/inquiry/RegistForm.tsx | 138 ++++--- src/components/inquiry/list/ListTable.tsx | 75 ++-- src/config/config.local.ts | 2 +- src/styles/components/_sub.scss | 443 +++++++++++----------- 9 files changed, 381 insertions(+), 403 deletions(-) delete mode 100644 src/components/inquiry/ListTable.tsx diff --git a/.env.development b/.env.development index a62742c..d821f10 100644 --- a/.env.development +++ b/.env.development @@ -2,15 +2,14 @@ NEXT_PUBLIC_RUN_MODE=development # 모바일 디바이스로 로컬 서버 확인하려면 자신 IP 주소로 변경 # 다시 로컬에서 개발할때는 localhost로 변경 #route handler -NEXT_PUBLIC_API_URL=http://172.30.1.23:3000 +NEXT_PUBLIC_API_URL=http://localhost:3000 #qsp 로그인 api NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 # NEXT_PUBLIC_QSP_API_URL=https://jp-dev.qsalesplatform.com #1:1문의 api -NEXT_PUBLIC_INQUIRY_API_URL=http://172.23.4.129:8110 -NEXT_PUBLIC_INQUIRY_FILE_DOWNLOAD_API_URL=https://jp-dev.qsalesplatform.com +NEXT_PUBLIC_INQUIRY_API_URL=https://jp-dev.qsalesplatform.com #QPARTNER 로그인 api diff --git a/.env.localhost b/.env.localhost index 2935f56..1480c7a 100644 --- a/.env.localhost +++ b/.env.localhost @@ -2,15 +2,14 @@ NEXT_PUBLIC_RUN_MODE=local # 모바일 디바이스로 로컬 서버 확인하려면 자신 IP 주소로 변경 # 다시 로컬에서 개발할때는 localhost로 변경 #route handler -NEXT_PUBLIC_API_URL=http://172.30.1.23:3000 +NEXT_PUBLIC_API_URL=http://localhost:3000 #qsp 로그인 api NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120 # NEXT_PUBLIC_QSP_API_URL=https://jp-dev.qsalesplatform.com #1:1문의 api -NEXT_PUBLIC_INQUIRY_API_URL=http://172.23.4.129:8110 -NEXT_PUBLIC_INQUIRY_FILE_DOWNLOAD_API_URL=https://jp-dev.qsalesplatform.com +NEXT_PUBLIC_INQUIRY_API_URL=https://jp-dev.qsalesplatform.com #QPARTNER 로그인 api DB_HOST=202.218.61.226 diff --git a/src/app/api/qna/file/route.ts b/src/app/api/qna/file/route.ts index 38aaf1b..7149b28 100644 --- a/src/app/api/qna/file/route.ts +++ b/src/app/api/qna/file/route.ts @@ -10,7 +10,7 @@ export async function GET(request: Request) { return NextResponse.json({ error: 'encodeFileNo is required' }, { status: 400 }) } try { - const response = await axios.get(`${process.env.NEXT_PUBLIC_INQUIRY_FILE_DOWNLOAD_API_URL}/api/file/downloadFile2`, { + const response = await axios.get(`${process.env.NEXT_PUBLIC_INQUIRY_API_URL}/api/file/downloadFile2`, { responseType: 'arraybuffer', params: { encodeFileNo, diff --git a/src/components/inquiry/Detail.tsx b/src/components/inquiry/Detail.tsx index 9e5d971..0aae351 100644 --- a/src/components/inquiry/Detail.tsx +++ b/src/components/inquiry/Detail.tsx @@ -3,6 +3,7 @@ import Answer from './Answer' import { useInquiry } from '@/hooks/useInquiry' import { useParams, useRouter } from 'next/navigation' +import { useSessionStore } from '@/store/session' export default function Detail() { const params = useParams() @@ -12,6 +13,8 @@ export default function Detail() { const { commonCodeList } = useInquiry() const router = useRouter() + const { session } = useSessionStore() + return ( <>
    @@ -30,24 +33,30 @@ export default function Detail() { 登録日 - {inquiryDetail?.regDt} + {inquiryDetail?.regDt.split(' ')[0]} - 作者 - {inquiryDetail?.regUserNm} + 顧客名 + + {session?.userNm} {session?.builderNo ? `[${session?.builderNo}]` : ''} + 販売店 {inquiryDetail?.storeNm} - - 施工店 - {inquiryDetail?.compCd} - E-mail {inquiryDetail?.regEmail} + + 名前 + {inquiryDetail?.regUserNm} + + + お問い合わせ + {inquiryDetail?.regUserTelNo} +
    diff --git a/src/components/inquiry/ListTable.tsx b/src/components/inquiry/ListTable.tsx deleted file mode 100644 index 95911a6..0000000 --- a/src/components/inquiry/ListTable.tsx +++ /dev/null @@ -1,91 +0,0 @@ -'use client' -export default function ListTable() { - return ( - <> -
    -
    -
    -
    - - -
    -
    -
    - -
    -
    -
    -
    - 合計 98個 -
    -
      -
    • -
      -
      - 屋根 - 適合性 - 屋根材 -
      -
      屋根材適合性確認依頼
      -
      2025.04.02
      -
      回答待ち
      -
      -
    • -
    • -
      -
      - 屋根 - 適合性 - 屋根材 -
      -
      設置可能ですか?
      -
      2025.04.02
      -
      回答完了
      -
      -
    • -
    • -
      -
      - 屋根 - 適合性 - 屋根材 -
      -
      屋根材適合性確認依頼屋根材適合性確認依頼屋根材適合性確認依頼屋根材適合性確認依頼
      -
      2025.04.02
      -
      回答待ち
      -
      -
    • -
    • -
      -
      - 屋根 - 適合性 - 屋根材 -
      -
      設置可能ですか?
      -
      2025.04.02
      -
      回答完了
      -
      -
    • -
    • -
      -
      조회된 데이터가 없습니다
      -
      -
    • -
    -
    - -
    -
    -
    - - ) -} diff --git a/src/components/inquiry/RegistForm.tsx b/src/components/inquiry/RegistForm.tsx index 0c3f452..93df62f 100644 --- a/src/components/inquiry/RegistForm.tsx +++ b/src/components/inquiry/RegistForm.tsx @@ -36,7 +36,13 @@ export default function RegistForm() { useEffect(() => { if (session?.isLoggedIn) { - setInquiryRequest({ ...inquiryRequest, regId: session?.userId ?? '', regUserNm: session?.userNm ?? '', storeId: session?.storeId ?? '' }) + setInquiryRequest({ + ...inquiryRequest, + regId: session?.userId ?? '', + regUserNm: session?.userNm ?? '', + storeId: session?.storeId ?? '', + qstMail: session?.email ?? '', + }) } }, [session]) @@ -74,6 +80,17 @@ export default function RegistForm() { focusOnRequiredField('qstMail') return } + if (inquiryRequest.title.length > 100) { + alert('お問い合わせタイトルは100文字以内で入力してください。') + focusOnRequiredField('title') + return + } + if (inquiryRequest.contents.length > 2000) { + alert('お問い合わせ内容は2,000文字以内で入力してください。') + focusOnRequiredField('contents') + return + } + const formData = new FormData() attachedFiles.forEach((file) => { formData.append('files', file) @@ -92,6 +109,21 @@ export default function RegistForm() { ) } + const handlePhoneNumberChange = (e: React.ChangeEvent) => { + const value = e.target.value.replace(/[^\d]/g, '') + + let formattedNumber = '' + if (value.length <= 3) { + formattedNumber = value + } else if (value.length <= 7) { + formattedNumber = `${value.slice(0, 3)}-${value.slice(3)}` + } else { + formattedNumber = `${value.slice(0, 3)}-${value.slice(3, 7)}-${value.slice(7, 11)}` + } + + setInquiryRequest({ ...inquiryRequest, regUserTelNo: formattedNumber }) + } + return ( <>
    @@ -120,46 +152,50 @@ export default function RegistForm() { ))}
    -
    - -
    -
    - -
    + {commonCodeList.filter((code) => code.refChar1 === inquiryRequest.qnaClsLrgCd).length > 0 && ( +
    + +
    + )} + {commonCodeList.filter((code) => code.refChar1 === inquiryRequest.qnaClsMidCd).length > 0 && ( +
    + +
    + )}
    @@ -181,10 +217,13 @@ export default function RegistForm() {
    setInquiryRequest({ ...inquiryRequest, regUserTelNo: e.target.value })} + onChange={handlePhoneNumberChange} + value={inquiryRequest.regUserTelNo ?? ''} id="regUserTelNo" + maxLength={13} />
    @@ -197,6 +236,7 @@ export default function RegistForm() { className="input-frame" type="text" placeholder="E-mailを書いてください" + value={inquiryRequest.qstMail} onChange={(e) => setInquiryRequest({ ...inquiryRequest, qstMail: e.target.value })} id="qstMail" /> @@ -212,21 +252,24 @@ export default function RegistForm() { type="text" placeholder="お問い合わせタイトルを記入してください" onChange={(e) => setInquiryRequest({ ...inquiryRequest, title: e.target.value })} + maxLength={100} id="title" />
    - お問い合わせタイプ * + お問い合わせ内容 *
    @@ -253,7 +296,10 @@ export default function RegistForm() { ))}
    -
    +
    + diff --git a/src/components/inquiry/list/ListTable.tsx b/src/components/inquiry/list/ListTable.tsx index d01de9d..cbb88af 100644 --- a/src/components/inquiry/list/ListTable.tsx +++ b/src/components/inquiry/list/ListTable.tsx @@ -13,12 +13,12 @@ const badgeStyle = [ { id: 'Y', label: '回答完了', - color: 'blue', + color: 'orange', }, { id: 'N', label: '回答待ち', - color: 'orange', + color: 'blue', }, ] export default function ListTable() { @@ -58,7 +58,11 @@ export default function ListTable() { }, [session, inquiryList]) const handleMyInquiry = () => { - setInquiryListRequest({ ...inquiryListRequest, schRegId: inquiryListRequest.schRegId ? null : session.userId }) + setOffset(1) + setInquiryListRequest({ + ...inquiryListRequest, + schRegId: inquiryListRequest.schRegId ? null : session.userId, + }) } const handleFilter = (e: React.ChangeEvent) => { @@ -98,39 +102,40 @@ export default function ListTable() {
    合計 {heldInquiryList.length > 0 ? heldInquiryList[0].totCnt : 0}
    - {heldInquiryList.length === 0 || (heldInquiryList.length > 0 && heldInquiryList[0].totCnt === 0) ? ( -
    -
    照会されたデータがありません。
    -
    - ) : ( -
      - {heldInquiryList.length > 0 && - heldInquiryList.map((inquiry: InquiryList) => ( -
    • router.push(`/inquiry/${inquiry.qnaNo}`)}> -
      -
      - {inquiry.qnaClsLrgCd} - {inquiry.qnaClsMidCd} - {inquiry.qnaClsSmlCd} -
      -
      {inquiry.qstTitle}
      -
      {inquiry.regDt}
      -
      badge.id === inquiry.answerYn)?.color}`}> - {badgeStyle.find((badge) => badge.id === inquiry.answerYn)?.label} -
      +
        + {heldInquiryList.length === 0 || (heldInquiryList.length > 0 && heldInquiryList[0].totCnt === 0) ? ( +
      • +
        +
        照会されたデータがありません。
        +
        +
      • + ) : ( + heldInquiryList.map((inquiry: InquiryList) => ( +
      • router.push(`/inquiry/${inquiry.qnaNo}`)}> +
        +
        + {inquiry.qnaClsLrgCd} + {inquiry.qnaClsMidCd} + {inquiry.qnaClsSmlCd}
        -
      • - ))} -
      - )} -
      - { - setOffset(offset + 10) - }} - /> -
      +
      {inquiry.qstTitle}
      +
      {inquiry.regDt}
      +
      badge.id === inquiry.answerYn)?.color}`}> + {badgeStyle.find((badge) => badge.id === inquiry.answerYn)?.label} +
      +
      +
    • + )) + )} +
    +
    +
    + { + setOffset(offset + 10) + }} + />
    diff --git a/src/config/config.local.ts b/src/config/config.local.ts index 8fbf68b..ac02c97 100644 --- a/src/config/config.local.ts +++ b/src/config/config.local.ts @@ -1,7 +1,7 @@ import getConfigs from '@/config/config.common' // 환경마다 달라져야 할 변수, 값들을 정의합니다. (여기는 local 환경에 맞는 값을 지정합니다.) -const baseUrl = 'http://172.30.1.23:3000' +const baseUrl = 'http://localhost:3000' const mode = 'local' // 환경마다 달라져야 할 값들을 getConfig 함수에 전달합니다. diff --git a/src/styles/components/_sub.scss b/src/styles/components/_sub.scss index 73c5fc4..939c66e 100644 --- a/src/styles/components/_sub.scss +++ b/src/styles/components/_sub.scss @@ -1,38 +1,38 @@ -@use "../abstracts" as *; +@use '../abstracts' as *; // input form 공통 -.data-input-form-bx{ +.data-input-form-bx { margin-bottom: 18px; - &:last-child{ + &:last-child { margin-bottom: 0; } - .data-input-form-tit{ + .data-input-form-tit { @include defaultFont($font-s-13, $font-w-500, $font-c); margin-bottom: 10px; - .import{ - color: #F00; + .import { + color: #f00; } - span{ + span { display: block; - @include defaultFont($font-s-13, $font-w-400, #A8B6C7); + @include defaultFont($font-s-13, $font-w-400, #a8b6c7); } } - .data-input-guide{ + .data-input-guide { margin-top: 8px; - @include defaultFont($font-s-13, $font-w-400, #A8B6C7); + @include defaultFont($font-s-13, $font-w-400, #a8b6c7); } -} +} -.btn-flex-wrap{ +.btn-flex-wrap { @include flex(5px); margin-top: 24px; - .btn-bx{ + .btn-bx { flex: 1; } - &.com{ - .btn-bx{ + &.com { + .btn-bx { flex: 1 1 auto; - button{ + button { font-size: 12px; } } @@ -40,13 +40,13 @@ } // 매물 common -.top-btn{ +.top-btn { position: fixed; bottom: 96px; right: 15px; width: 38px; height: 38px; - background-color: rgba(0, 0, 0, 0.50); + background-color: rgba(0, 0, 0, 0.5); background-image: url(/assets/images/sub/top_btn_icon.svg); background-position: center; background-repeat: no-repeat; @@ -55,68 +55,68 @@ z-index: 90000; } -.sale-contents{ +.sale-contents { width: 100%; - background-color: #F5F5F5; - .sale-frame{ + background-color: #f5f5f5; + .sale-frame { padding: 0 20px; - border-top: 1px solid #ECECEC; - border-bottom: 1px solid #ECECEC; + border-top: 1px solid #ececec; + border-bottom: 1px solid #ececec; margin-bottom: 10px; padding-bottom: 24px; padding-top: 24px; background-color: $white-fff; - &:first-child{ + &:first-child { padding-top: 0; border-top: none; } - &:last-child{ + &:last-child { padding-bottom: 0; border-bottom: none; margin-bottom: 0; } } } -.sale-form-btn-wrap{ - padding: 20px 20px 0 ; +.sale-form-btn-wrap { + padding: 20px 20px 0; background-color: #fff; - .btn-flex-wrap{ + .btn-flex-wrap { margin-top: 0; } } // 매물 목록 -.sale-form-bx{ +.sale-form-bx { margin-bottom: 14px; - &:last-child{ + &:last-child { margin-bottom: 0; } -} -.sale-list-wrap{ - .sale-list-item{ +} +.sale-list-wrap { + .sale-list-item { padding-top: 14px; padding-bottom: 14px; - border-bottom: 1px solid #ECECEC; + border-bottom: 1px solid #ececec; cursor: pointer; - &:first-child{ + &:first-child { padding-top: 0; } - &:last-child{ + &:last-child { border-bottom: none; padding-bottom: 0; } } } -.sale-item-bx{ - .sale-item-date-bx{ +.sale-item-bx { + .sale-item-date-bx { @include flex(0px); align-items: center; margin-bottom: 9px; - .sale-item-num{ + .sale-item-num { position: relative; @include defaultFont($font-s-13, $font-w-400, $font-c); padding-right: 6px; - &::after{ + &::after { content: ''; position: absolute; top: 50%; @@ -124,31 +124,31 @@ transform: translateY(-50%); width: 1px; height: 10px; - background-color: #A2ABB8; + background-color: #a2abb8; } } - .sale-item-date{ - @include defaultFont($font-s-13, $font-w-400, #A2ABB8); + .sale-item-date { + @include defaultFont($font-s-13, $font-w-400, #a2abb8); padding-left: 6px; } } - .sale-item-tit{ + .sale-item-tit { @include defaultFont($font-s-15, $font-w-500, $font-c); @include ellipsis(1); margin-bottom: 9px; } - .sale-item-customer{ + .sale-item-customer { @include defaultFont($font-s-13, $font-w-400, $font-c); margin-bottom: 9px; } - .sale-item-update-bx{ + .sale-item-update-bx { @include flex(0px); align-items: center; - .sale-item-name{ + .sale-item-name { position: relative; - @include defaultFont($font-s-13, $font-w-400, #A2ABB8); + @include defaultFont($font-s-13, $font-w-400, #a2abb8); padding-right: 6px; - &::after{ + &::after { content: ''; position: absolute; top: 50%; @@ -156,176 +156,177 @@ transform: translateY(-50%); width: 1px; height: 10px; - background-color: #A2ABB8; + background-color: #a2abb8; } } - .sale-item-update{ - @include defaultFont($font-s-13, $font-w-400, #A2ABB8); + .sale-item-update { + @include defaultFont($font-s-13, $font-w-400, #a2abb8); padding-left: 6px; } } - &.nodata{ - .sale-item-nodata{ + &.nodata { + .sale-item-nodata { padding: 5px 0; text-align: center; @include defaultFont($font-s-15, $font-w-500, $font-c); } } } -.sale-edit-btn{ +.sale-edit-btn { margin-top: 24px; } // 매물 상세 -.sale-data-table-wrap{ +.sale-data-table-wrap { padding: 24px; background-color: #fff; - border-top: 1px solid #ECECEC; + border-top: 1px solid #ececec; } -.sale-data-table{ +.sale-data-table { width: 100%; table-layout: fixed; - tbody{ - tr{ - th{ + tbody { + tr { + th { @include defaultFont($font-s-13, $font-w-500, $font-c); vertical-align: top; padding: 5px 0; } - td{ + td { @include defaultFont($font-s-13, $font-w-400, $font-c); padding: 5px 0 8px 14px; - .data-down{ + .data-down { @include flex(8px); align-items: center; - color: #1259CB; - i{ + color: #1259cb; + i { display: block; width: 8px; height: 12px; - background: url(/assets/images/sub/down_icon.svg)no-repeat center; + background: url(/assets/images/sub/down_icon.svg) no-repeat center; background-size: cover; } } } - &:first-child{ - th,td{ + &:first-child { + th, + td { padding-top: 0; } } - &:last-child{ - th,td{ + &:last-child { + th, + td { padding-bottom: 0; } } } } -} +} -.sale-detail-toggle-wrap{ - border-top: 1px solid #ECECEC; +.sale-detail-toggle-wrap { + border-top: 1px solid #ececec; } -.sale-detail-toggle-bx{ - border-bottom: 1px solid #ECECEC; +.sale-detail-toggle-bx { + border-bottom: 1px solid #ececec; } -.sale-detail-toggle-head{ +.sale-detail-toggle-head { @include flex(5px); padding: 14px 18px; background-color: $white-fff; cursor: pointer; - .sale-detail-toggle-name{ + .sale-detail-toggle-name { @include defaultFont($font-s-13, $font-w-500, $font-c); } - .sale-detail-toggle-btn-wrap{ + .sale-detail-toggle-btn-wrap { margin-left: auto; - .sale-detail-toggle-btn{ + .sale-detail-toggle-btn { display: block; width: 22px; height: 22px; - background: url(/assets/images/sub/sale_toggle_btn.svg)no-repeat center; - background-size: cover + background: url(/assets/images/sub/sale_toggle_btn.svg) no-repeat center; + background-size: cover; } } } -.sale-detail-toggle-cont{ +.sale-detail-toggle-cont { display: none; - .sale-frame{ + .sale-frame { padding: 24px 20px; - &:first-child{ + &:first-child { padding-top: 24px; } - &:last-child{ + &:last-child { padding-bottom: 24px; } } } -.sale-detail-toggle-bx{ - &.act{ - .sale-detail-toggle-head{ - background-color: #5F738E; - .sale-detail-toggle-name{ - color: #fff +.sale-detail-toggle-bx { + &.act { + .sale-detail-toggle-head { + background-color: #5f738e; + .sale-detail-toggle-name { + color: #fff; } - .sale-detail-toggle-btn-wrap{ - .sale-detail-toggle-btn{ - background: url(/assets/images/sub/sale_toggle_btn_white.svg)no-repeat center; + .sale-detail-toggle-btn-wrap { + .sale-detail-toggle-btn { + background: url(/assets/images/sub/sale_toggle_btn_white.svg) no-repeat center; } } } - .sale-detail-toggle-cont{ + .sale-detail-toggle-cont { display: block; } } } // 매물 기본정보 -.form-flex{ +.form-flex { @include flex(5px); - .form-bx{ + .form-bx { flex: 1; } } -.form-btn{ +.form-btn { margin-top: 12px; } // 매물 전기 지붕정보 -.sale-roof-title{ +.sale-roof-title { @include defaultFont($font-s-15, $font-w-500, $font-c); padding-bottom: 10px; margin-bottom: 20px; - border-bottom: 1px solid #2E3A59; + border-bottom: 1px solid #2e3a59; } -.data-check-wrap{ +.data-check-wrap { @include flex(10px); flex-wrap: wrap; margin-bottom: 12px; .radio-form-box, - .check-form-box{ + .check-form-box { width: calc(50% - 5px); } - &.mb0{ + &.mb0 { margin-bottom: 0; } } -.data-input{ - &.flex{ +.data-input { + &.flex { @include flex(8px); align-items: center; - span{ + span { flex: none; @include defaultFont($font-s-13, $font-w-400, $font-c); } } } - // 1:1 문의 common -.inquiry-frame{ +.inquiry-frame { padding: 0 20px; } -.badge{ +.badge { min-width: 60px; height: 30px; line-height: 30px; @@ -334,65 +335,64 @@ text-align: center; font-size: $font-s-12; font-weight: $font-w-500; - &.blue{ - color: #5497E9; - background-color: #ECF5FF; + &.blue { + color: #5497e9; + background-color: #ecf5ff; } - &.orange{ - color: #F86A56; - background-color: #FFEFED; + &.orange { + color: #f86a56; + background-color: #ffefed; } - &.block{ + &.block { width: 100%; - } } // 1:1 문의 목록 -.inquiry-table-filter{ +.inquiry-table-filter { margin-bottom: 24px; - .filter-check{ + .filter-check { margin-bottom: 12px; } } -.inquiry-list-tit{ +.inquiry-list-tit { padding-bottom: 10px; - border-bottom: 1px solid #2E3A59; + border-bottom: 1px solid #2e3a59; @include defaultFont($font-s-13, $font-w-400, $font-c); - span{ + span { font-weight: $font-w-500; } } -.inquiry-list{ - .inquiry-item{ +.inquiry-list { + .inquiry-item { padding: 10px 0; cursor: pointer; - border-bottom: 1px solid #ECECEC; - &:last-child{ + border-bottom: 1px solid #ececec; + &:last-child { border-bottom: none; padding-bottom: 0; } - .inquiry-item-bx{ + .inquiry-item-bx { position: relative; padding-right: 70px; - .inquiry-item-category{ + .inquiry-item-category { display: flex; align-items: center; margin-bottom: 5px; - span{ + span { position: relative; display: block; @include defaultFont($font-s-13, $font-w-400, $font-c); padding: 0 6px; - &:first-child{ + &:first-child { padding-left: 0; } - &:last-child{ + &:last-child { padding-right: 0; - &::before{ + &::before { display: none; } } - &::before{ + &::before { content: ''; position: absolute; top: 50%; @@ -400,26 +400,31 @@ transform: translateY(-50%); width: 1px; height: 10px; - background-color: #A2ABB8; + background-color: #a2abb8; } } } - .inquiry-item-tit{ + .inquiry-item-tit { @include defaultFont($font-s-15, $font-w-500, $font-c); @include ellipsis(1); margin-bottom: 5px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + display: block; } - .inquiry-item-date{ - @include defaultFont($font-s-13, $font-w-400, #A2ABB8); + .inquiry-item-date { + @include defaultFont($font-s-13, $font-w-400, #a2abb8); } - .inquiry-badge{ + .inquiry-badge { position: absolute; top: 0; right: 0; } - &.nodata{ + &.nodata { padding-right: 0; - .inquiry-item-nodata{ + .inquiry-item-nodata { padding: 10px 0; text-align: center; @include defaultFont($font-s-15, $font-w-500, $font-c); @@ -430,42 +435,45 @@ } // 1:1문의 작성 -.inquiry-file-wrap{ +.textarea-form { + white-space: pre-wrap; +} +.inquiry-file-wrap { margin-top: 20px; - .file-list-wrap{ + .file-list-wrap { margin-top: 14px; } } -.file-list-tit{ +.file-list-tit { @include defaultFont($font-s-13, $font-w-500, $font-c); } -.file-list{ +.file-list { margin-top: 14px; - .file-item{ - border-top: 1px solid #EDEDED; + .file-item { + border-top: 1px solid #ededed; cursor: default; - .file-item-bx{ + .file-item-bx { width: 100%; padding: 14px 0; @include flex(0px); align-items: center; - .file-item-name{ + .file-item-name { @include ellipsis(1); @include defaultFont($font-s-13, $font-w-400, $font-c); padding-right: 10px; } - .file-del{ + .file-del { flex: none; display: block; margin-left: auto; width: 16px; height: 16px; - background: url(/assets/images/common/id_delete_icon.svg)no-repeat center; + background: url(/assets/images/common/id_delete_icon.svg) no-repeat center; background-size: cover; } } - &:last-child{ - .file-item-bx{ + &:last-child { + .file-item-bx { padding-bottom: 0; } } @@ -473,33 +481,33 @@ } // 1:1 문의 상세 -.inquiry-detail-data-table{ +.inquiry-detail-data-table { padding: 20px 0; - border-bottom: 1px solid #ECECEC; + border-bottom: 1px solid #ececec; } -.inquiry-detail-data{ +.inquiry-detail-data { padding: 20px 0; - border-bottom: 1px solid #2E3A59; + border-bottom: 1px solid #2e3a59; margin-bottom: 24px; - .inquiry-detail-category{ + .inquiry-detail-category { display: flex; align-items: center; margin-bottom: 3px; - span{ + span { position: relative; display: block; @include defaultFont($font-s-13, $font-w-400, $font-c); padding: 0 6px; - &:first-child{ + &:first-child { padding-left: 0; } - &:last-child{ + &:last-child { padding-right: 0; - &::before{ + &::before { display: none; } } - &::before{ + &::before { content: ''; position: absolute; top: 50%; @@ -507,151 +515,154 @@ transform: translateY(-50%); width: 1px; height: 10px; - background-color: #A2ABB8; + background-color: #a2abb8; } } } - .inquiry-detail-tit{ + .inquiry-detail-tit { @include defaultFont($font-s-15, $font-w-500, $font-c); margin-bottom: 10px; + word-wrap: break-word; + white-space: normal; + overflow-wrap: break-word; } - .inquiry-detail-txt{ + .inquiry-detail-txt { @include defaultFont($font-s-13, $font-w-400, $font-c); + white-space: pre-line; } } // 1:1 문의 답변 -.inquiry-answer-wrap{ +.inquiry-answer-wrap { margin-top: 24px; - } -.inquiry-answer-header{ +.inquiry-answer-header { padding: 20px 0; - border-top: 1px solid #F86A56; - border-bottom: 1px solid #ECECEC; - .inquiry-answer-tit{ - @include defaultFont($font-s-14, $font-w-500, #F86A56); + border-top: 1px solid #f86a56; + border-bottom: 1px solid #ececec; + .inquiry-answer-tit { + @include defaultFont($font-s-14, $font-w-500, #f86a56); margin-bottom: 5px; } - .inquiry-answer-date{ - @include defaultFont($font-s-13, $font-w-400, #F86A56); + .inquiry-answer-date { + @include defaultFont($font-s-13, $font-w-400, #f86a56); } } -.inquiry-answer-tit{ +.inquiry-answer-tit { @include defaultFont($font-s-13, $font-w-400, $font-c); margin-bottom: 3px; } // 비밀번호 변경 -.border-frame{ +.border-frame { padding: 20px; - border-top: 1px solid #ECECEC; - border-bottom: 1px solid #ECECEC; + border-top: 1px solid #ececec; + border-bottom: 1px solid #ececec; background-color: #fff; margin-bottom: 10px; - &:last-child{ + &:last-child { border-bottom: none; padding-bottom: 0; margin-bottom: 0; } } -.pw-guide{ - .pw-guide-tit{ - @include defaultFont($font-s-16, $font-w-500, #1259CB); +.pw-guide { + .pw-guide-tit { + @include defaultFont($font-s-16, $font-w-500, #1259cb); } - .pw-guide-txt{ - @include defaultFont($font-s-13, $font-w-400, #417DDC); + .pw-guide-txt { + @include defaultFont($font-s-13, $font-w-400, #417ddc); } } // 지붕재 적합성 -.compliance-icon{ +.compliance-icon { display: flex; } -.compliance-check-wrap{ +.compliance-check-wrap { padding-top: 10px; } -.compliance-check-bx{ +.compliance-check-bx { position: relative; padding: 14px 18px; - border: 1px solid #EFEFEF; + border: 1px solid #efefef; border-radius: 4px; margin-bottom: 10px; - &:last-child{ + &:last-child { margin-bottom: 0; } - &.act{ - .bx-btn{ + &.act { + .bx-btn { transform: rotate(0) !important; } - .reference-list{ - display: block + .reference-list { + display: block; } } } -.check-name-wrap{ +.check-name-wrap { @include flex(0px); align-items: center; - .check-name{ + .check-name { @include defaultFont($font-s-13, $font-w-500, $font-c); } - .check-name-btn{ + .check-name-btn { padding-left: 5px; margin-left: auto; - .bx-btn{ + .bx-btn { display: block; width: 22px; height: 22px; - background: url(/assets/images/sub/compliance_bx_icon.svg)no-repeat center; + background: url(/assets/images/sub/compliance_bx_icon.svg) no-repeat center; transform: rotate(180deg); } } } -.reference-list{ +.reference-list { display: none; margin-top: 10px; padding-top: 14px; - border-top: 1px solid #ECECEC; - transition: all .15s ease-in-out; - .reference-item{ + border-top: 1px solid #ececec; + transition: all 0.15s ease-in-out; + .reference-item { margin-bottom: 8px; padding-left: 14px; - .reference-item-bx{ + .reference-item-bx { @include flex(10px); @include defaultFont($font-s-13, $font-w-400, $font-c); align-items: center; } - &:last-child{ + &:last-child { margin-bottom: 0; } } - &.check{ - .reference-item{ + &.check { + .reference-item { margin-bottom: 14px; } } } -.compliace-nosearch{ +.compliace-nosearch { padding: 30px 0; - span{ + span { display: block; @include defaultFont($font-s-13, $font-w-400, $font-c); text-align: center; } } -.check-item-wrap{ +.check-item-wrap { @include flex(0px); align-items: center; } -.compliance-icon-wrap{ +.compliance-icon-wrap { margin-left: auto; min-width: 44px; @include flex(0px); align-items: center; } -.float-btn-wrap{ +.float-btn-wrap { position: sticky; bottom: 10px; left: 0; @@ -659,14 +670,14 @@ background-color: #fff; z-index: 9; } -@media screen and (max-width: 360px){ - .btn-flex-wrap{ +@media screen and (max-width: 360px) { + .btn-flex-wrap { flex-direction: column; } - .data-check-wrap{ + .data-check-wrap { .radio-form-box, - .check-form-box{ + .check-form-box { width: 100%; } } -} \ No newline at end of file +} From 7f14135616c4dca19eb33dae1691377825e4cc96 Mon Sep 17 00:00:00 2001 From: keyy1315 Date: Thu, 29 May 2025 13:47:07 +0900 Subject: [PATCH 19/19] fix: resolve file download error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - content-type이 text/html 일 경우에만 에러 핸들링, 이외 타입은 octet-stream으로 반환 --- src/app/api/qna/file/route.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app/api/qna/file/route.ts b/src/app/api/qna/file/route.ts index 7149b28..01930f2 100644 --- a/src/app/api/qna/file/route.ts +++ b/src/app/api/qna/file/route.ts @@ -4,6 +4,7 @@ import { NextResponse } from 'next/server' export async function GET(request: Request) { const { searchParams } = new URL(request.url) const encodeFileNo = searchParams.get('encodeFileNo') + const srcFileNm = searchParams.get('srcFileNm') if (!encodeFileNo) { @@ -16,17 +17,16 @@ export async function GET(request: Request) { encodeFileNo, }, }) - if (response.headers['content-type'] === 'application/octet-stream;charset=UTF-8') { - return new NextResponse(response.data, { - status: 200, - headers: { - 'Content-Type': 'application/octet-stream;charset=UTF-8', - 'Content-Disposition': `attachment; filename="${srcFileNm}"`, - }, - }) - } else { + if (response.headers['content-type'] === 'text/html;charset=utf-8') { return NextResponse.json({ error: 'file not found' }, { status: 404 }) } + return new NextResponse(response.data, { + status: 200, + headers: { + 'Content-Type': 'application/octet-stream;charset=UTF-8', + 'Content-Disposition': `attachment; filename="${srcFileNm}"`, + }, + }) } catch (error: any) { return NextResponse.json({ error: error.response.data }, { status: 500 }) }