refactor: enhance error handling and user feedback in survey components

- API try-catch 구문 함수 구현
- 조사 매물 alert 처리 리팩토링
This commit is contained in:
Dayoung 2025-06-18 10:43:12 +09:00
parent 1a42848ae9
commit 12b9dd4216
8 changed files with 188 additions and 88 deletions

View File

@ -3,6 +3,24 @@ import { NextResponse } from 'next/server'
import { loggerWrapper } from '@/libs/api-wrapper' import { loggerWrapper } from '@/libs/api-wrapper'
import { ERROR_MESSAGES } from '@/utils/common-utils' import { ERROR_MESSAGES } from '@/utils/common-utils'
/**
* @api {GET} /api/qna/file API
* @apiName GET /api/qna/file
* @apiGroup Qna
* @apiDescription API
*
* @apiParam {String} encodeFileNo
* @apiParam {String} srcFileNm
*
* @apiExample {curl} Example usage:
* curl -X GET http://localhost:3000/api/qna/file?encodeFileNo=1234567890&srcFileNm=test.pdf
*
* @apiSuccessExample {octet-stream} Success-Response:
* file content
*
* @apiError {Number} 500
* @apiError {Number} 400
*/
async function downloadFile(request: Request): Promise<NextResponse> { async function downloadFile(request: Request): Promise<NextResponse> {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url)
const encodeFileNo = searchParams.get('encodeFileNo') const encodeFileNo = searchParams.get('encodeFileNo')

View File

@ -41,22 +41,17 @@ import { loggerWrapper } from '@/libs/api-wrapper'
* @apiError {String} error.message * @apiError {String} error.message
*/ */
async function getSubmitTargetData(request: NextRequest): Promise<NextResponse> { async function getSubmitTargetData(request: NextRequest): Promise<NextResponse> {
try { const { searchParams } = new URL(request.url)
const { searchParams } = new URL(request.url) const storeId = searchParams.get('storeId')
const storeId = searchParams.get('storeId') const role = searchParams.get('role')
const role = searchParams.get('role')
if (!storeId || !role) { if (!storeId || !role) {
return NextResponse.json({ error: ERROR_MESSAGES.BAD_REQUEST }, { status: HttpStatusCode.BadRequest }) return NextResponse.json({ error: ERROR_MESSAGES.BAD_REQUEST }, { status: HttpStatusCode.BadRequest })
}
const submissionService = new SubmissionService(storeId, role)
const data = await submissionService.getSubmissionTarget()
return NextResponse.json(data)
} catch (error) {
console.error('❌ API ROUTE ERROR:', error)
return NextResponse.json({ error: ERROR_MESSAGES.FETCH_ERROR }, { status: HttpStatusCode.InternalServerError })
} }
const submissionService = new SubmissionService(storeId, role)
const data = await submissionService.tryFunction(() => submissionService.getSubmissionTarget())
return NextResponse.json(data)
} }
export const GET = loggerWrapper(getSubmitTargetData) export const GET = loggerWrapper(getSubmitTargetData)

View File

@ -1,5 +1,8 @@
import { prisma } from '@/libs/prisma' import { prisma } from '@/libs/prisma'
import { ERROR_MESSAGES } from '@/utils/common-utils'
import { SubmitTargetResponse } from '@/types/Survey' import { SubmitTargetResponse } from '@/types/Survey'
import { HttpStatusCode } from 'axios'
import { NextResponse } from 'next/server'
export class SubmissionService { export class SubmissionService {
private storeId: string private storeId: string
@ -70,4 +73,17 @@ export class SubmissionService {
` `
return await prisma.$queryRawUnsafe(this.BASE_QUERY + query, this.storeId) return await prisma.$queryRawUnsafe(this.BASE_QUERY + query, this.storeId)
} }
handleRouteError(error: unknown): NextResponse {
console.error('❌ API ROUTE ERROR : ', error)
return NextResponse.json({ error: ERROR_MESSAGES.FETCH_ERROR }, { status: HttpStatusCode.InternalServerError })
}
async tryFunction(func: () => Promise<any>): Promise<any> {
try {
return await func()
} catch (error) {
return this.handleRouteError(error)
}
}
} }

View File

@ -6,6 +6,7 @@ import { useEffect, useState } from 'react'
import { useParams, useRouter } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
import { requiredFields, useSurvey } from '@/hooks/useSurvey' import { requiredFields, useSurvey } from '@/hooks/useSurvey'
import { usePopupController } from '@/store/popupController' import { usePopupController } from '@/store/popupController'
import { ALERT_MESSAGES } from '@/types/Survey'
interface ButtonFormProps { interface ButtonFormProps {
mode: Mode mode: Mode
@ -48,7 +49,7 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) {
const isSubmit = data.basic.submissionStatus const isSubmit = data.basic.submissionStatus
const { deleteSurvey, updateSurvey, isDeletingSurvey, isUpdatingSurvey } = useSurvey(id) const { deleteSurvey, updateSurvey, isDeletingSurvey, isUpdatingSurvey } = useSurvey(id)
const { validateSurveyDetail, createSurvey, isCreatingSurvey } = useSurvey() const { validateSurveyDetail, createSurvey, isCreatingSurvey, showSurveyAlert, showSurveyConfirm } = useSurvey()
useEffect(() => { useEffect(() => {
if (!session?.isLoggedIn) return if (!session?.isLoggedIn) return
@ -116,7 +117,7 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) {
router.push(`/survey-sale/${savedId}`) router.push(`/survey-sale/${savedId}`)
} }
} }
alert('一時保存されました。') showSurveyAlert(ALERT_MESSAGES.TEMP_SAVE_SUCCESS)
} }
/** 입력 필드 포커스 처리 */ /** 입력 필드 포커스 처리 */
@ -128,7 +129,13 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) {
/** 저장 로직 */ /** 저장 로직 */
const saveProcess = async (emptyField: string | null, isSubmitProcess?: boolean) => { const saveProcess = async (emptyField: string | null, isSubmitProcess?: boolean) => {
if (emptyField?.trim() === '') { if (emptyField?.trim() === '') {
await handleSuccessfulSave(isSubmitProcess) if (!isSubmitProcess) {
showSurveyConfirm(ALERT_MESSAGES.SAVE_CONFIRM, async () => {
await handleSuccessfulSave(isSubmitProcess)
})
} else {
await handleSuccessfulSave(isSubmitProcess)
}
} else { } else {
handleFailedSave(emptyField) handleFailedSave(emptyField)
} }
@ -147,6 +154,8 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) {
setMode('READ') setMode('READ')
if (isSubmitProcess) { if (isSubmitProcess) {
popupController.setSurveySaleSubmitPopup(true) popupController.setSurveySaleSubmitPopup(true)
} else {
showSurveyAlert(ALERT_MESSAGES.SAVE_SUCCESS)
} }
} }
} else { } else {
@ -156,7 +165,7 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) {
await router.push(`/survey-sale/${savedId}?show=true`) await router.push(`/survey-sale/${savedId}?show=true`)
} else { } else {
await router.push(`/survey-sale/${savedId}`) await router.push(`/survey-sale/${savedId}`)
alert('保存されました。') showSurveyAlert(ALERT_MESSAGES.SAVE_SUCCESS)
} }
} }
} }
@ -164,9 +173,10 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) {
/** 필수값 미입력 처리 */ /** 필수값 미입력 처리 */
const handleFailedSave = (emptyField: string | null) => { const handleFailedSave = (emptyField: string | null) => {
if (emptyField?.includes('Unit')) { if (emptyField?.includes('Unit')) {
alert('電気契約容量の単位を入力してください。') showSurveyAlert(ALERT_MESSAGES.UNIT_REQUIRED)
} else { } else {
alert(requiredFields.find((field) => field.field === emptyField)?.name + ' 項目が空です。') const fieldInfo = requiredFields.find((field) => field.field === emptyField)
showSurveyAlert(ALERT_MESSAGES.REQUIRED_FIELD, fieldInfo?.name || '')
} }
focusInput(emptyField as keyof SurveyDetailInfo) focusInput(emptyField as keyof SurveyDetailInfo)
} }
@ -174,10 +184,10 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) {
/** 삭제 로직 */ /** 삭제 로직 */
const handleDelete = async () => { const handleDelete = async () => {
if (!Number.isNaN(id)) { if (!Number.isNaN(id)) {
window.neoConfirm('削除しますか?', async () => { showSurveyConfirm(ALERT_MESSAGES.DELETE_CONFIRM, async () => {
await deleteSurvey() await deleteSurvey()
if (!isDeletingSurvey) { if (!isDeletingSurvey) {
alert('削除されました。') showSurveyAlert(ALERT_MESSAGES.DELETE_SUCCESS)
router.push('/survey-sale') router.push('/survey-sale')
} }
}) })
@ -187,16 +197,16 @@ export default function ButtonForm({ mode, setMode, data }: ButtonFormProps) {
/** 제출 로직 */ /** 제출 로직 */
const handleSubmit = async () => { const handleSubmit = async () => {
if (data.basic.srlNo?.startsWith('一時保存') && Number.isNaN(id)) { if (data.basic.srlNo?.startsWith('一時保存') && Number.isNaN(id)) {
alert('一時保存されたデータは提出できません。') showSurveyAlert(ALERT_MESSAGES.TEMP_SAVE_SUBMIT_ERROR)
return return
} }
if (mode === 'READ') { if (mode === 'READ') {
window.neoConfirm('提出しますか?', async () => { showSurveyConfirm(ALERT_MESSAGES.SUBMIT_CONFIRM, async () => {
popupController.setSurveySaleSubmitPopup(true) popupController.setSurveySaleSubmitPopup(true)
}) })
} else { } else {
window.neoConfirm('記入した情報を保存して送信しますか?', async () => { showSurveyConfirm(ALERT_MESSAGES.SAVE_AND_SUBMIT_CONFIRM, async () => {
handleSave(false, true) handleSave(false, true)
}) })
} }

View File

@ -1,5 +1,7 @@
import { useState } from 'react' import { useState } from 'react'
import type { Mode, SurveyDetailInfo, SurveyDetailRequest } from '@/types/Survey' import type { Mode, SurveyDetailInfo, SurveyDetailRequest } from '@/types/Survey'
import { useSurvey } from '@/hooks/useSurvey'
import { ALERT_MESSAGES } from '@/types/Survey'
type RadioEtcKeys = type RadioEtcKeys =
| 'structureOrder' | 'structureOrder'
@ -247,6 +249,7 @@ export default function RoofForm(props: {
mode: Mode mode: Mode
}) { }) {
const { roofInfo, setRoofInfo, mode } = props const { roofInfo, setRoofInfo, mode } = props
const { showSurveyAlert } = useSurvey()
const [isFlip, setIsFlip] = useState<boolean>(true) const [isFlip, setIsFlip] = useState<boolean>(true)
const handleNumberInput = (key: keyof SurveyDetailRequest, value: number | string) => { const handleNumberInput = (key: keyof SurveyDetailRequest, value: number | string) => {
@ -254,13 +257,13 @@ export default function RoofForm(props: {
if (key === 'roofSlope' || key === 'openFieldPlateThickness') { if (key === 'roofSlope' || key === 'openFieldPlateThickness') {
const stringValue = value.toString() const stringValue = value.toString()
if (stringValue.length > 5) { if (stringValue.length > 5) {
alert('保存できるサイズを超えました。') showSurveyAlert('保存できるサイズを超えました。')
return return
} }
if (stringValue.includes('.')) { if (stringValue.includes('.')) {
const decimalPlaces = stringValue.split('.')[1].length const decimalPlaces = stringValue.split('.')[1].length
if (decimalPlaces > 1) { if (decimalPlaces > 1) {
alert('小数点以下1桁までしか許されません。') showSurveyAlert('小数点以下1桁までしか許されません。')
return return
} }
} }
@ -732,6 +735,7 @@ const MultiCheck = ({
roofInfo: SurveyDetailInfo roofInfo: SurveyDetailInfo
setRoofInfo: (roofInfo: SurveyDetailRequest) => void setRoofInfo: (roofInfo: SurveyDetailRequest) => void
}) => { }) => {
const { showSurveyAlert } = useSurvey()
const multiCheckData = column === 'supplementaryFacilities' ? supplementaryFacilities : roofMaterial const multiCheckData = column === 'supplementaryFacilities' ? supplementaryFacilities : roofMaterial
const etcValue = roofInfo?.[`${column}Etc` as keyof SurveyDetailInfo] const etcValue = roofInfo?.[`${column}Etc` as keyof SurveyDetailInfo]
const [isOtherCheck, setIsOtherCheck] = useState<boolean>(Boolean(etcValue)) const [isOtherCheck, setIsOtherCheck] = useState<boolean>(Boolean(etcValue))
@ -751,7 +755,7 @@ const MultiCheck = ({
if (isRoofMaterial) { if (isRoofMaterial) {
const totalSelected = selectedValues.length + (isOtherSelected || isOtherCheck ? 1 : 0) const totalSelected = selectedValues.length + (isOtherSelected || isOtherCheck ? 1 : 0)
if (totalSelected >= 2) { if (totalSelected >= 2) {
alert('屋根材は最大2個まで選択できます。') showSurveyAlert(ALERT_MESSAGES.ROOF_MATERIAL_MAX_SELECT_ERROR)
return return
} }
} }
@ -765,7 +769,7 @@ const MultiCheck = ({
if (isRoofMaterial) { if (isRoofMaterial) {
const currentSelected = selectedValues.length const currentSelected = selectedValues.length
if (!isOtherCheck && currentSelected >= 2) { if (!isOtherCheck && currentSelected >= 2) {
alert('屋根材は最大2個まで選択できます。') showSurveyAlert(ALERT_MESSAGES.ROOF_MATERIAL_MAX_SELECT_ERROR)
return return
} }
} }

View File

@ -3,16 +3,18 @@
import { SEARCH_OPTIONS, SEARCH_OPTIONS_ENUM, SEARCH_OPTIONS_PARTNERS, useSurveyFilterStore } from '@/store/surveyFilterStore' import { SEARCH_OPTIONS, SEARCH_OPTIONS_ENUM, SEARCH_OPTIONS_PARTNERS, useSurveyFilterStore } from '@/store/surveyFilterStore'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useState } from 'react' import { useState } from 'react'
import { useSurvey } from '@/hooks/useSurvey'
export default function SearchForm({ memberRole, userId }: { memberRole: string; userId: string }) { export default function SearchForm({ memberRole, userId }: { memberRole: string; userId: string }) {
const router = useRouter() const router = useRouter()
const { showSurveyAlert } = useSurvey()
const { setSearchOption, setSort, setIsMySurvey, setKeyword, reset, isMySurvey, keyword, searchOption, sort, setOffset } = useSurveyFilterStore() const { setSearchOption, setSort, setIsMySurvey, setKeyword, reset, isMySurvey, keyword, searchOption, sort, setOffset } = useSurveyFilterStore()
const [searchKeyword, setSearchKeyword] = useState(keyword) const [searchKeyword, setSearchKeyword] = useState(keyword)
const [option, setOption] = useState(searchOption) const [option, setOption] = useState(searchOption)
const handleSearch = () => { const handleSearch = () => {
if (option !== 'id' && searchKeyword.trim().length < 2) { if (option !== 'id' && searchKeyword.trim().length < 2) {
alert('2文字以上入力してください') showSurveyAlert('2文字以上入力してください')
return return
} }
setOffset(0) setOffset(0)
@ -62,7 +64,7 @@ export default function SearchForm({ memberRole, userId }: { memberRole: string;
placeholder="タイトルを入力してください. (2文字以上)" placeholder="タイトルを入力してください. (2文字以上)"
onChange={(e) => { onChange={(e) => {
if (e.target.value.length > 30) { if (e.target.value.length > 30) {
alert('30文字以内で入力してください') showSurveyAlert('30文字以内で入力してください')
return return
} }
setSearchKeyword(e.target.value) setSearchKeyword(e.target.value)

View File

@ -1,4 +1,4 @@
import type { SubmitTargetResponse, SurveyBasicInfo, SurveyDetailRequest, SurveyRegistRequest } from '@/types/Survey' import { ALERT_MESSAGES, type SubmitTargetResponse, type SurveyBasicInfo, type SurveyDetailRequest, type SurveyRegistRequest } from '@/types/Survey'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useSurveyFilterStore } from '@/store/surveyFilterStore' import { useSurveyFilterStore } from '@/store/surveyFilterStore'
@ -93,6 +93,8 @@ export function useSurvey(
refetchSurveyList: () => void refetchSurveyList: () => void
refetchSurveyDetail: () => void refetchSurveyDetail: () => void
getSubmitTarget: (params: { storeId: string; role: string }) => Promise<SubmitTargetResponse[] | null> getSubmitTarget: (params: { storeId: string; role: string }) => Promise<SubmitTargetResponse[] | null>
showSurveyAlert: (message: (typeof ALERT_MESSAGES)[keyof typeof ALERT_MESSAGES] | string, requiredField?: string) => void
showSurveyConfirm: (message: string, onConfirm: () => void, onCancel?: () => void) => void
} { } {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { keyword, searchOption, isMySurvey, sort, offset } = useSurveyFilterStore() const { keyword, searchOption, isMySurvey, sort, offset } = useSurveyFilterStore()
@ -111,7 +113,7 @@ export function useSurvey(
const errorMsg = error.response?.data.error const errorMsg = error.response?.data.error
console.error('❌ API ERROR : ', error) console.error('❌ API ERROR : ', error)
if (errorMsg) { if (errorMsg) {
alert(errorMsg) showSurveyAlert(errorMsg)
} }
if (isThrow) { if (isThrow) {
throw new Error(error) throw new Error(error)
@ -138,6 +140,19 @@ export function useSurvey(
} }
} }
const tryFunction = async (func: () => Promise<any>, isList?: boolean, isThrow?: boolean): Promise<any> => {
try {
const resp = await func()
return resp.data
} catch (error) {
handleError(error, isThrow)
if (isList) {
return { data: [], count: 0 }
}
return null
}
}
/** /**
* @description * @description
* *
@ -154,24 +169,23 @@ export function useSurvey(
} = useQuery({ } = useQuery({
queryKey: ['survey', 'list', keyword, searchOption, isMySurvey, sort, offset, session?.storeNm, session?.builderId, session?.role], queryKey: ['survey', 'list', keyword, searchOption, isMySurvey, sort, offset, session?.storeNm, session?.builderId, session?.role],
queryFn: async () => { queryFn: async () => {
try { return await tryFunction(
const resp = await axiosInstance(null).get<{ data: SurveyBasicInfo[]; count: number }>('/api/survey-sales', { () =>
params: { axiosInstance(null).get<{ data: SurveyBasicInfo[]; count: number }>('/api/survey-sales', {
keyword, params: {
searchOption, keyword,
isMySurvey, searchOption,
sort, isMySurvey,
offset, sort,
storeId: session?.storeId, offset,
builderId: session?.builderId, storeId: session?.storeId,
role: session?.role, builderId: session?.builderId,
}, role: session?.role,
}) },
return resp.data }),
} catch (error: any) { true,
handleError(error, false) false,
return { data: [], count: 0 } )
}
}, },
}) })
@ -205,17 +219,16 @@ export function useSurvey(
queryKey: ['survey', id], queryKey: ['survey', id],
queryFn: async () => { queryFn: async () => {
if (Number.isNaN(id) || id === undefined || id === 0) return null if (Number.isNaN(id) || id === undefined || id === 0) return null
try { return await tryFunction(
const resp = await axiosInstance(null).get<SurveyBasicInfo>(`/api/survey-sales/${id}`, { () =>
params: { axiosInstance(null).get<SurveyBasicInfo>(`/api/survey-sales/${id}`, {
isPdf: isPdf, params: {
}, isPdf: isPdf,
}) },
return resp.data }),
} catch (error: any) { false,
handleError(error, false) false,
return null )
}
}, },
enabled: id !== 0 && id !== undefined && id !== null, enabled: id !== 0 && id !== undefined && id !== null,
}) })
@ -294,7 +307,7 @@ export function useSurvey(
queryClient.invalidateQueries({ queryKey: ['survey', 'list'] }) queryClient.invalidateQueries({ queryKey: ['survey', 'list'] })
}, },
onError: (error: any) => { onError: (error: any) => {
alert(error.response?.data.error) handleError(error, true)
}, },
}) })
@ -378,15 +391,12 @@ export function useSurvey(
* @throws {Error} * @throws {Error}
*/ */
const getZipCode = async (zipCode: string): Promise<ZipCode[] | null> => { const getZipCode = async (zipCode: string): Promise<ZipCode[] | null> => {
try { const data = await tryFunction(
const { data } = await axiosInstance(null).get<ZipCodeResponse>( () => axiosInstance(null).get<ZipCodeResponse>(`https://zipcloud.ibsnet.co.jp/api/search?${queryStringFormatter({ zipcode: zipCode.trim() })}`),
`https://zipcloud.ibsnet.co.jp/api/search?${queryStringFormatter({ zipcode: zipCode.trim() })}`, false,
) true,
return data.results )
} catch (error: any) { return data ? data.results : null
handleError(error, true)
return null
}
} }
/** /**
@ -398,24 +408,38 @@ export function useSurvey(
* @returns {Promise<SubmitTargetResponse[]|null>} * @returns {Promise<SubmitTargetResponse[]|null>}
*/ */
const getSubmitTarget = async (params: { storeId: string; role: string }): Promise<SubmitTargetResponse[] | null> => { const getSubmitTarget = async (params: { storeId: string; role: string }): Promise<SubmitTargetResponse[] | null> => {
try { if (!params.storeId) {
if (!params.storeId) { /** 판매점 ID 없는 경우 */
/** 판매점 ID 없는 경우 */ showSurveyAlert('販売店IDがありません。')
alert('販売店IDがありません。')
return null
}
const endpoint = `/api/submission?storeId=${params.storeId}&role=${params.role}`
if (!endpoint) {
/** 권한 오류 */
alert('権限が間違っています。')
return null
}
const { data } = await axiosInstance(null).get<SubmitTargetResponse[]>(endpoint)
return data
} catch (error: any) {
handleError(error, true)
return null return null
} }
const endpoint = `/api/submission?storeId=${params.storeId}&role=${params.role}`
if (!endpoint) {
/** 권한 오류 */
showSurveyAlert('権限が間違っています。')
return null
}
return await tryFunction(() => axiosInstance(null).get<SubmitTargetResponse[]>(endpoint), false, true)
}
const showSurveyAlert = (message: (typeof ALERT_MESSAGES)[keyof typeof ALERT_MESSAGES] | string, requiredField?: string) => {
if (requiredField) {
alert(`${requiredField} ${message}`)
} else {
alert(message)
}
}
const showSurveyConfirm = (message: string, onConfirm: () => void, onCancel?: () => void) => {
if (window.neoConfirm) {
window.neoConfirm(message, onConfirm)
} else {
const confirmed = confirm(message)
if (confirmed) {
onConfirm()
} else if (onCancel) {
onCancel()
}
}
} }
return { return {
@ -436,5 +460,7 @@ export function useSurvey(
getSubmitTarget, getSubmitTarget,
refetchSurveyList, refetchSurveyList,
refetchSurveyDetail, refetchSurveyDetail,
showSurveyAlert,
showSurveyConfirm,
} }
} }

View File

@ -323,3 +323,32 @@ export type SurveySearchParams = {
/** 시공점 ID */ /** 시공점 ID */
builderId?: string | null builderId?: string | null
} }
export const ALERT_MESSAGES = {
/** 기본 메세지 */
/** 저장 성공 - "저장되었습니다." */
SAVE_SUCCESS: '保存されました。',
/** 임시 저장 성공 - "임시 저장되었습니다." */
TEMP_SAVE_SUCCESS: '一時保存されました。',
/** 삭제 성공 - "삭제되었습니다." */
DELETE_SUCCESS: '削除されました。',
/** 제출 확인 - "제출하시겠습니까?" */
SUBMIT_CONFIRM: '提出しますか?',
/** 저장 확인 - "저장하시겠습니까?" */
SAVE_CONFIRM: '保存しますか?',
/** 삭제 확인 - "삭제하시겠습니까?" */
DELETE_CONFIRM: '削除しますか?',
/** 저장 및 제출 확인 - "입력한 정보를 저장하고 보내시겠습니까?" */
SAVE_AND_SUBMIT_CONFIRM: '記入した情報を保存して送信しますか?',
/** 임시 저장 제출 오류 - "임시 저장된 데이터는 제출할 수 없습니다." */
TEMP_SAVE_SUBMIT_ERROR: '一時保存されたデータは提出できません。',
/** 입력 오류 메세지 */
/* 전기계약 용량 단위 입력 메세지 - "전기 계약 용량의 단위를 입력하세요."*/
UNIT_REQUIRED: '電気契約容量の単位を入力してください。',
/** 필수 입력 메세지 - "항목이 비어 있습니다."*/
REQUIRED_FIELD: '項目が空です。',
/** 최대 선택 오류 메세지 - "지붕재는 최대 2개까지 선택할 수 있습니다." */
ROOF_MATERIAL_MAX_SELECT_ERROR: '屋根材は最大2個まで選択できます。',
}