feature/survey #71

Merged
swyoo merged 9 commits from feature/survey into dev 2025-06-17 13:43:25 +09:00
12 changed files with 87 additions and 70 deletions

View File

@ -1,13 +1,8 @@
import { prisma } from '@/libs/prisma' import { prisma } from '@/libs/prisma'
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { loggerWrapper } from '@/libs/api-wrapper' import { loggerWrapper } from '@/libs/api-wrapper'
import { SubmitTargetResponse } from '@/types/Survey'
type AdminSubPerson = {
storeId: string
userId: string
eMail: string
authority: string
}
// 2차점이 자신에게 매핑 된 1차 판매점과 관리자 정보 조회 // 2차점이 자신에게 매핑 된 1차 판매점과 관리자 정보 조회
async function getSubMissionAdminSub(request: NextRequest): Promise<NextResponse> { async function getSubMissionAdminSub(request: NextRequest): Promise<NextResponse> {
try { try {
@ -33,12 +28,11 @@ async function getSubMissionAdminSub(request: NextRequest): Promise<NextResponse
AND MCS.DEL_YN = 'N'; AND MCS.DEL_YN = 'N';
CLOSE SYMMETRIC KEY SYMMETRICKEY; CLOSE SYMMETRIC KEY SYMMETRICKEY;
` `
const suitable: AdminSubPerson[] = await prisma.$queryRawUnsafe(query) const data: SubmitTargetResponse[] = await prisma.$queryRawUnsafe(query)
return NextResponse.json(data)
return NextResponse.json({ message: 'Hello, world!' })
} catch (error) { } catch (error) {
console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error)
return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) return NextResponse.json({ error: 'データの取得に失敗しました。' }, { status: 500 })
} }
} }

View File

@ -37,12 +37,11 @@ async function getSubmissionAdmin(request: NextRequest): Promise<NextResponse> {
; ;
CLOSE SYMMETRIC KEY SYMMETRICKEY; CLOSE SYMMETRIC KEY SYMMETRICKEY;
` `
const suitable: SuperPerson[] = await prisma.$queryRawUnsafe(query) const data: SuperPerson[] = await prisma.$queryRawUnsafe(query)
return NextResponse.json(data)
return NextResponse.json({ message: 'Hello, world!' })
} catch (error) { } catch (error) {
console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error)
return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) return NextResponse.json({ error: 'データの取得に失敗しました。' }, { status: 500 })
} }
} }

View File

@ -3,13 +3,6 @@ import { SubmitTargetResponse } from '@/types/Survey'
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { loggerWrapper } from '@/libs/api-wrapper' import { loggerWrapper } from '@/libs/api-wrapper'
type BuilderPerson = {
agencyStoreId: string
userId: string
eMail: string
userAuthCd: string
}
// 2차점의 시공권한 user가 해당 판매점의 관리자 정보 조회 // 2차점의 시공권한 user가 해당 판매점의 관리자 정보 조회
// N == 일반유저, S == 수퍼유저, B == 시공권한유저 // N == 일반유저, S == 수퍼유저, B == 시공권한유저
async function getSubmissionBuilder(request: NextRequest): Promise<NextResponse> { async function getSubmissionBuilder(request: NextRequest): Promise<NextResponse> {
@ -37,14 +30,11 @@ async function getSubmissionBuilder(request: NextRequest): Promise<NextResponse>
AND MCAS.DEL_YN = 'N'; AND MCAS.DEL_YN = 'N';
CLOSE SYMMETRIC KEY SYMMETRICKEY; CLOSE SYMMETRIC KEY SYMMETRICKEY;
` `
// const suitable: BuilderPerson[] = await prisma.$queryRawUnsafe(query)
// return NextResponse.json({ message: 'Hello, world!' })
const data: SubmitTargetResponse[] = await prisma.$queryRawUnsafe(query) const data: SubmitTargetResponse[] = await prisma.$queryRawUnsafe(query)
return NextResponse.json(data) return NextResponse.json(data)
} catch (error) { } catch (error) {
console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error)
return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) return NextResponse.json({ error: 'データの取得に失敗しました。' }, { status: 500 })
} }
} }

View File

@ -30,12 +30,11 @@ async function getSubmissionSuper(request: NextRequest): Promise<NextResponse> {
AND MCSA.DEL_YN = 'N'; AND MCSA.DEL_YN = 'N';
CLOSE SYMMETRIC KEY SYMMETRICKEY; CLOSE SYMMETRIC KEY SYMMETRICKEY;
` `
const suitable: SuperPerson[] = await prisma.$queryRawUnsafe(query) const data: SuperPerson[] = await prisma.$queryRawUnsafe(query)
return NextResponse.json(data)
return NextResponse.json({ message: 'Hello, world!' })
} catch (error) { } catch (error) {
console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error) console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error);
return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 }) return NextResponse.json({ error: 'データの取得に失敗しました。' }, { status: 500 });
} }
} }

View File

@ -6,6 +6,7 @@ import { sessionOptions } from '@/libs/session'
import { cookies } from 'next/headers' import { cookies } from 'next/headers'
import type { SessionData } from '@/types/Auth' import type { SessionData } from '@/types/Auth'
import { Prisma } from '@prisma/client' import { Prisma } from '@prisma/client'
import { loggerWrapper } from '@/libs/api-wrapper'
/** /**
* @description * @description
@ -128,11 +129,11 @@ const fetchSurvey = async (id: number) => {
* ... * ...
* } * }
*/ */
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { async function getSurveySaleDetail(request: NextRequest): Promise<NextResponse> {
try { try {
const cookieStore = await cookies() const cookieStore = await cookies()
const session = await getIronSession<SessionData>(cookieStore, sessionOptions) const session = await getIronSession<SessionData>(cookieStore, sessionOptions)
const { id } = await params const id = request.nextUrl.pathname.split('/').pop() ?? ''
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)
const isPdf = searchParams.get('isPdf') === 'true' const isPdf = searchParams.get('isPdf') === 'true'
@ -228,9 +229,9 @@ const getNewSrlNo = async (srlNo: string, storeId: string, role: string) => {
* ... * ...
* } * }
* */ * */
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { async function updateSurveySaleDetail(request: NextRequest): Promise<NextResponse> {
try { try {
const { id } = await params const id = request.nextUrl.pathname.split('/').pop() ?? ''
const body = await request.json() const body = await request.json()
const { detailInfo, ...basicInfo } = body.survey const { detailInfo, ...basicInfo } = body.survey
@ -257,6 +258,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: 'データ保存に失敗しました。' }, { status: 500 }) return NextResponse.json({ error: 'データ保存に失敗しました。' }, { status: 500 })
} }
} }
/** /**
* @api {DELETE} /api/survey-sales/:id API * @api {DELETE} /api/survey-sales/:id API
* @apiName DELETE /api/survey-sales/:id * @apiName DELETE /api/survey-sales/:id
@ -275,9 +277,9 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
* { * {
* "message": "success" * "message": "success"
*/ */
export async function DELETE(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { async function deleteSurveySaleDetail(request: NextRequest): Promise<NextResponse> {
try { try {
const { id } = await params const id = request.nextUrl.pathname.split('/').pop() ?? ''
await prisma.$transaction(async (tx: Prisma.TransactionClient) => { await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
// @ts-ignore // @ts-ignore
@ -343,9 +345,9 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
* } * }
* } * }
*/ */
export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { async function submitSurveySaleDetail(request: NextRequest): Promise<NextResponse> {
try { try {
const { id } = await params const id = request.nextUrl.pathname.split('/').pop() ?? ''
const body = await request.json() const body = await request.json()
// @ts-ignore // @ts-ignore
const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({ const survey = await prisma.SD_SURVEY_SALES_BASIC_INFO.update({
@ -364,3 +366,8 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
return NextResponse.json({ error: 'データ保存に失敗しました。' }, { status: 500 }) return NextResponse.json({ error: 'データ保存に失敗しました。' }, { status: 500 })
} }
} }
export const GET = loggerWrapper(getSurveySaleDetail)
export const PUT = loggerWrapper(updateSurveySaleDetail)
export const DELETE = loggerWrapper(deleteSurveySaleDetail)
export const PATCH = loggerWrapper(submitSurveySaleDetail)

View File

@ -1,6 +1,7 @@
import { NextResponse } from 'next/server' import { NextResponse } from 'next/server'
import { prisma } from '@/libs/prisma' import { prisma } from '@/libs/prisma'
import { convertToSnakeCase } from '@/utils/common-utils' import { convertToSnakeCase } from '@/utils/common-utils'
import { loggerWrapper } from '@/libs/api-wrapper'
/** /**
* @description * @description
*/ */
@ -192,7 +193,7 @@ const checkSession = (params: SearchParams) => {
* } * }
* *
*/ */
export async function GET(request: Request) { async function getSurveySales(request: Request) {
try { try {
/** URL 파라미터 파싱 */ /** URL 파라미터 파싱 */
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)
@ -273,7 +274,7 @@ export async function GET(request: Request) {
* *
* @apiError {Number} 500 * @apiError {Number} 500
*/ */
export async function PUT(request: Request) { async function updateSurveySales(request: Request) {
try { try {
/** 요청 바디 파싱 */ /** 요청 바디 파싱 */
const body = await request.json() const body = await request.json()
@ -335,7 +336,7 @@ export async function PUT(request: Request) {
* *
* @apiError {Number} 500 * @apiError {Number} 500
*/ */
export async function POST(request: Request) { async function createSurveySales(request: Request) {
try { try {
const body = await request.json() const body = await request.json()
@ -393,3 +394,7 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'データ保存に失敗しました。' }, { status: 500 }) return NextResponse.json({ error: 'データ保存に失敗しました。' }, { status: 500 })
} }
} }
export const GET = loggerWrapper(getSurveySales)
export const PUT = loggerWrapper(updateSurveySales)
export const POST = loggerWrapper(createSurveySales)

View File

@ -48,34 +48,43 @@ export default function SurveySaleSubmitPopup() {
}) })
const [commCodeList, setCommCodeList] = useState<CommCode[]>([]) const [commCodeList, setCommCodeList] = useState<CommCode[]>([])
/** 제출 타겟 데이터 조회 및 제출 폼 데이터 삽입 */
useEffect(() => { useEffect(() => {
if (!session?.isLoggedIn || !surveyDetail?.id) return if (!session?.isLoggedIn || !surveyDetail?.id) return
const baseUpdate = {
sender: session?.email ?? '',
title: '[HANASYS現地調査] 調査物件が提出. (' + surveyDetail?.srlNo + ')',
}
if (session?.role === 'Admin') { if (session?.role === 'Admin') {
getCommCode('SALES_OFFICE_CD').then((codes) => { getCommCode('SALES_OFFICE_CD').then((codes) => {
setCommCodeList(codes) setCommCodeList(codes)
}) })
setSubmitData((prev) => ({
...prev,
...baseUpdate,
}))
} else if (session?.role === 'Builder' || session?.role === 'Admin_Sub') { } else if (session?.role === 'Builder' || session?.role === 'Admin_Sub') {
getSubmitTarget({ storeId: surveyDetail?.storeId ?? '', role: session?.role ?? '' }).then((data) => { getSubmitTarget({ storeId: surveyDetail?.storeId ?? '', role: session?.role ?? '' }).then((data) => {
if (data) { if (!data) return
setSubmitData({ if (data && data.length > 0) {
...submitData, const updateData: Partial<SubmitFormData> = {
targetId: data[0].targetStoreId, sender: session?.email ?? '',
targetNm: data[0].targetStoreNm, title: '[HANASYS現地調査] 調査物件が提出. (' + surveyDetail?.srlNo + ')',
}) targetId: data[0]?.targetStoreId ?? '',
data.length > 1 && targetNm: data[0]?.targetStoreNm ?? '',
setSubmitData({ receiver: data.filter((item) => item.auth === 'S').map((item) => item.repUserEmail),
...submitData, reference: data.filter((item) => item.auth === 'N').map((item) => item.repUserEmail),
receiver: data.filter((item) => item.auth === 'S').map((item) => item.repUserEmail), saleBase: null,
reference: data.filter((item) => item.auth === 'N').map((item) => item.repUserEmail), contents: '',
}) }
setSubmitData((prev) => ({
...prev,
...updateData,
}))
} }
}) })
} }
setSubmitData({
...submitData,
sender: session?.email ?? '',
title: '[HANASYS現地調査] 調査物件が提出. (' + surveyDetail?.srlNo + ')',
})
}, [session, surveyDetail]) }, [session, surveyDetail])
const FORM_FIELDS: FormField[] = [ const FORM_FIELDS: FormField[] = [
@ -118,6 +127,7 @@ export default function SurveySaleSubmitPopup() {
setIsShow(true) setIsShow(true)
sendEmail({ sendEmail({
to: submitData.receiver, to: submitData.receiver,
cc: submitData.reference ?? '',
subject: submitData.title, subject: submitData.title,
content: contentsRef.current?.innerHTML ?? '', content: contentsRef.current?.innerHTML ?? '',
}) })

View File

@ -90,10 +90,9 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) {
/** 저장 로직 */ /** 저장 로직 */
const handleSave = (isTemporary: boolean, isSubmitProcess: boolean) => { const handleSave = (isTemporary: boolean, isSubmitProcess: boolean) => {
const emptyField = validateSurveyDetail(data.roof) const emptyField = validateSurveyDetail(data.roof)
const hasEmptyField = emptyField?.trim() !== ''
if (isTemporary) { if (isTemporary) {
hasEmptyField ? tempSaveProcess() : saveProcess(emptyField, false) tempSaveProcess()
} else { } else {
saveProcess(emptyField, isSubmitProcess) saveProcess(emptyField, isSubmitProcess)
} }

View File

@ -94,6 +94,14 @@ export default function DetailForm() {
})) }))
const [roofInfoData, setRoofInfoData] = useState<SurveyDetailRequest>(roofInfoForm) const [roofInfoData, setRoofInfoData] = useState<SurveyDetailRequest>(roofInfoForm)
/** route 에러 처리 - 잘못된 URL 접근 시 생성 페이지로 리다이렉트 */
useEffect(() => {
if (modeset === 'CREATE' && pathname !== '/survey-sale/regist') {
router.replace('/survey-sale/regist')
}
return
}, [modeset, id, pathname])
/** 제출 팝업 처리 - createSurvey 이후 popup 처리 시 노드 삽입 오류로 인해 별도 처리 */ /** 제출 팝업 처리 - createSurvey 이후 popup 처리 시 노드 삽입 오류로 인해 별도 처리 */
useEffect(() => { useEffect(() => {
const show = searchParams.get('show') const show = searchParams.get('show')

View File

@ -48,7 +48,9 @@ export function useInquiry(
*/ */
const errorRouter = (error: any) => { const errorRouter = (error: any) => {
const status = error.response?.status const status = error.response?.status
alert(error.response?.data.error) if (error.response?.data.error) {
alert(error.response?.data.error)
}
switch (status) { switch (status) {
// session 없는 경우 // session 없는 경우
case 401: case 401:

View File

@ -108,7 +108,9 @@ export function useSurvey(
*/ */
const errorRouter = (error: any) => { const errorRouter = (error: any) => {
const status = error.response?.status const status = error.response?.status
alert(error.response?.data.error) if (error.response?.data.error) {
alert(error.response?.data.error)
}
switch (status) { switch (status) {
/** session 없는 경우 */ /** session 없는 경우 */
case 401: case 401:
@ -259,6 +261,9 @@ export function useSurvey(
queryClient.invalidateQueries({ queryKey: ['survey', id] }) queryClient.invalidateQueries({ queryKey: ['survey', id] })
queryClient.invalidateQueries({ queryKey: ['survey', 'list'] }) queryClient.invalidateQueries({ queryKey: ['survey', 'list'] })
}, },
onError: (error: any) => {
alert(error.response?.data.error)
},
}) })
/** /**
@ -269,7 +274,7 @@ export function useSurvey(
* *
* @example * @example
* *
* *
*/ */
const { mutateAsync: deleteSurvey, isPending: isDeletingSurvey } = useMutation({ const { mutateAsync: deleteSurvey, isPending: isDeletingSurvey } = useMutation({
mutationFn: async () => { mutationFn: async () => {
@ -370,8 +375,9 @@ export function useSurvey(
`https://zipcloud.ibsnet.co.jp/api/search?${queryStringFormatter({ zipcode: zipCode.trim() })}`, `https://zipcloud.ibsnet.co.jp/api/search?${queryStringFormatter({ zipcode: zipCode.trim() })}`,
) )
return data.results return data.results
} catch (e) { } catch (error: any) {
console.error('Failed to fetch zipcode data:', e) console.error('Failed to fetch zipcode data:', error)
alert(error.response?.data.error)
throw new Error('Failed to fetch zipcode data') throw new Error('Failed to fetch zipcode data')
} }
} }

View File

@ -1,8 +1,8 @@
export const useTitle = () => { export const useTitle = () => {
const getTitle = (pathname: string) => { const getTitle = (pathname: string) => {
// Handle dynamic routes first // Handle dynamic routes first
if (pathname.startsWith('/survey-sale/') && pathname !== '/survey-sale/basic-info' && pathname !== '/survey-sale/roof-info') { if (pathname.startsWith('/survey-sale/') && pathname !== '/survey-sale/regist') {
return '調査物件一覧' return '調査物件詳細'
} }
if (pathname.startsWith('/inquiry/') && pathname !== '/inquiry/list' && pathname !== '/inquiry/regist') { if (pathname.startsWith('/inquiry/') && pathname !== '/inquiry/list' && pathname !== '/inquiry/regist') {
@ -17,10 +17,8 @@ export const useTitle = () => {
return '屋根材適合性の確認' return '屋根材適合性の確認'
case '/survey-sale': case '/survey-sale':
return '調査物件一覧' return '調査物件一覧'
case '/survey-sale/basic-info': case '/survey-sale/regist':
return '調査物件登録' return '調査物件登録'
case '/survey-sale/roof-info':
return '調査物件新規登録'
case '/inquiry/list': case '/inquiry/list':
return '1:1お問い合わせ' return '1:1お問い合わせ'
case '/inquiry/regist': case '/inquiry/regist':