Compare commits

..

3 Commits

6 changed files with 94 additions and 46 deletions

View File

@ -9,7 +9,7 @@ import { SUITABLE_HEAD_CODE, type Suitable, type SuitableDetail } from '@/types/
export default function SuitableDetailPopup() { export default function SuitableDetailPopup() {
const popupController = usePopupController() const popupController = usePopupController()
const { toCodeName, toSuitableDetail, suitableCheckIcon, suitableCheckMemo, getSelectedSuitables } = useSuitable() const { toCodeName, toSuitableDetail, suitableCheckIcon, suitableCheckMemo, getSelectedSuitables, suitableErrorAlert } = useSuitable()
const [openItems, setOpenItems] = useState<Set<number>>(new Set()) const [openItems, setOpenItems] = useState<Set<number>>(new Set())
const [selectedSuitables, setSelectedSuitables] = useState<Suitable[]>([]) const [selectedSuitables, setSelectedSuitables] = useState<Suitable[]>([])
@ -24,9 +24,13 @@ export default function SuitableDetailPopup() {
}, []) }, [])
useEffect(() => { useEffect(() => {
getSelectedSuitables().then((res) => { getSelectedSuitables()
setSelectedSuitables(res) .then((res) => {
}) setSelectedSuitables(res)
})
.catch(() => {
suitableErrorAlert()
})
}, []) }, [])
return ( return (

View File

@ -6,12 +6,16 @@ import { useSuitableStore } from '@/store/useSuitableStore'
export default function SuitableButton() { export default function SuitableButton() {
const popupController = usePopupController() const popupController = usePopupController()
const { getSuitableIds, clearSuitableStore, downloadSuitablePdf } = useSuitable() const { getSuitableIds, clearSuitableStore, downloadSuitablePdf, suitableErrorAlert } = useSuitable()
const { selectedItems, addAllSelectedItem } = useSuitableStore() const { selectedItems, addAllSelectedItem } = useSuitableStore()
/* 데이터 전체 선택 */ /* 데이터 전체 선택 */
const handleSelectAll = async () => { const handleSelectAll = async () => {
addAllSelectedItem(await getSuitableIds()) try {
addAllSelectedItem(await getSuitableIds())
} catch (error) {
suitableErrorAlert()
}
} }
/* 상세 팝업 열기 */ /* 상세 팝업 열기 */

View File

@ -19,7 +19,9 @@ export default function SuitableList() {
hasNextPage, hasNextPage,
isFetchingNextPage, isFetchingNextPage,
isLoading, isLoading,
isError,
suitableCheckIcon, suitableCheckIcon,
suitableErrorAlert,
} = useSuitable() } = useSuitable()
const { selectedItems, addSelectedItem, removeSelectedItem } = useSuitableStore() const { selectedItems, addSelectedItem, removeSelectedItem } = useSuitableStore()
const [openItems, setOpenItems] = useState<Set<number>>(new Set()) const [openItems, setOpenItems] = useState<Set<number>>(new Set())
@ -144,6 +146,11 @@ export default function SuitableList() {
useSpinnerStore.getState().setIsShow(isLoading || isFetchingNextPage) useSpinnerStore.getState().setIsShow(isLoading || isFetchingNextPage)
}, [isLoading, isFetchingNextPage]) }, [isLoading, isFetchingNextPage])
/* 조회 데이터 오류 처리 */
useEffect(() => {
if (isError) suitableErrorAlert()
}, [isError])
/* 조회 데이터 없는 경우 */ /* 조회 데이터 없는 경우 */
if (!suitableList.length) return <SuitableNoData /> if (!suitableList.length) return <SuitableNoData />

View File

@ -9,7 +9,7 @@ export function useCommCode() {
return response.data return response.data
} catch (error) { } catch (error) {
console.error(`common code (${headCode}) load failed:`, error) console.error(`common code (${headCode}) load failed:`, error)
return [] throw error
} }
} }

View File

@ -51,7 +51,7 @@ export function useSuitable() {
return response.data return response.data
} catch (error) { } catch (error) {
console.error(`지붕재 적합성 데이터 조회 실패: ${error}`) console.error(`지붕재 적합성 데이터 조회 실패: ${error}`)
return [] throw error
} }
} }
@ -69,7 +69,7 @@ export function useSuitable() {
return response.data return response.data
} catch (error) { } catch (error) {
console.error(`지붕재 적합성 데이터 아이디 조회 실패: ${error}`) console.error(`지붕재 적합성 데이터 아이디 조회 실패: ${error}`)
return [] throw error
} }
} }
@ -89,7 +89,7 @@ export function useSuitable() {
return response.data return response.data
} catch (error) { } catch (error) {
console.error(`지붕재 적합성 상세 데이터 조회 실패: ${error}`) console.error(`지붕재 적합성 상세 데이터 조회 실패: ${error}`)
return [] throw error
} }
} }
@ -101,9 +101,13 @@ export function useSuitable() {
const getSuitableCommCode = (): void => { const getSuitableCommCode = (): void => {
const headCodes = Object.values(SUITABLE_HEAD_CODE) as SUITABLE_HEAD_CODE[] const headCodes = Object.values(SUITABLE_HEAD_CODE) as SUITABLE_HEAD_CODE[]
for (const code of headCodes) { for (const code of headCodes) {
getCommCode(code).then((res) => { getCommCode(code)
setSuitableCommCode(code, res) .then((res) => {
}) setSuitableCommCode(code, res)
})
.catch(() => {
suitableErrorAlert()
})
} }
} }
@ -169,8 +173,8 @@ export function useSuitable() {
hasNextPage, hasNextPage,
isFetchingNextPage, isFetchingNextPage,
isLoading, isLoading,
// isError, isError,
// error, error,
} = useInfiniteQuery<Suitable[]>({ } = useInfiniteQuery<Suitable[]>({
queryKey: ['suitables', 'list', searchCategory, searchKeyword], queryKey: ['suitables', 'list', searchCategory, searchKeyword],
queryFn: async (context) => { queryFn: async (context) => {
@ -189,6 +193,8 @@ export function useSuitable() {
staleTime: 1000 * 60 * 10, staleTime: 1000 * 60 * 10,
gcTime: 1000 * 60 * 10, gcTime: 1000 * 60 * 10,
enabled: searchCategory !== '' || searchKeyword !== '', enabled: searchCategory !== '' || searchKeyword !== '',
retry: 1,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
}) })
/** /**
@ -263,8 +269,12 @@ export function useSuitable() {
* @returns {Promise<Suitable[]>} * @returns {Promise<Suitable[]>}
*/ */
const getSelectedSuitables = async (): Promise<Suitable[]> => { const getSelectedSuitables = async (): Promise<Suitable[]> => {
const { ids, detailIds } = serializeSelectedItems() try {
return await getSuitableDetails(ids, detailIds) const { ids, detailIds } = serializeSelectedItems()
return await getSuitableDetails(ids, detailIds)
} catch (error) {
throw error
}
} }
/** /**
@ -308,11 +318,20 @@ export function useSuitable() {
document.body.removeChild(form) document.body.removeChild(form)
} catch (error) { } catch (error) {
console.error(`지붕재 적합성 상세 데이터 pdf 다운로드 실패: ${error}`) console.error(`지붕재 적합성 상세 데이터 pdf 다운로드 실패: ${error}`)
suitableErrorAlert()
} }
} }
/**
* @description
*
* @returns {void}
*/
const suitableErrorAlert = (): void => {
alert('一時的なエラーが発生しました。 継続的な場合は、管理者に連絡してください。')
}
return { return {
getSuitables,
getSuitableIds, getSuitableIds,
getSuitableCommCode, getSuitableCommCode,
toCodeName, toCodeName,
@ -323,10 +342,13 @@ export function useSuitable() {
hasNextPage, hasNextPage,
isFetchingNextPage, isFetchingNextPage,
isLoading, isLoading,
isError,
error,
getSelectedSuitables, getSelectedSuitables,
clearSuitableStore, clearSuitableStore,
suitableCheckIcon, suitableCheckIcon,
suitableCheckMemo, suitableCheckMemo,
downloadSuitablePdf, downloadSuitablePdf,
suitableErrorAlert,
} }
} }

View File

@ -15,9 +15,14 @@ interface ApiLogData {
body: string | undefined body: string | undefined
} }
/* 현재 날짜 반환 함수 (YYYY-MM-DD 형식) */
const getCurrentDate = (): string => {
return new Date().toISOString().split('T')[0]
}
/* 날짜별 로그 파일 경로 생성 함수 */ /* 날짜별 로그 파일 경로 생성 함수 */
const getLogFilePath = (): string => { const getLogFilePath = (): string => {
const today = new Date().toISOString().split('T')[0] // YYYY-MM-DD 형식 const today: string = getCurrentDate()
return join(process.cwd(), 'logs', `onsite-survey-${today}.log`) return join(process.cwd(), 'logs', `onsite-survey-${today}.log`)
} }
@ -25,15 +30,10 @@ const getLogFilePath = (): string => {
class DailyLogger { class DailyLogger {
private currentDate: string private currentDate: string
private logger: pino.Logger private logger: pino.Logger
private destination: ReturnType<typeof pino.destination> private destination?: ReturnType<typeof pino.destination>
constructor() { constructor() {
this.currentDate = new Date().toISOString().split('T')[0] this.currentDate = getCurrentDate()
this.destination = pino.destination({
dest: getLogFilePath(),
mkdir: true,
sync: false,
})
this.logger = this.createLogger() this.logger = this.createLogger()
/* kill signal 핸들러 등록 */ /* kill signal 핸들러 등록 */
@ -42,14 +42,31 @@ class DailyLogger {
} }
private async handleShutdown(): Promise<void> { private async handleShutdown(): Promise<void> {
this.destination.flushSync() if (isProduction && this.destination) {
this.destination.end() this.destination.flushSync()
this.destination.end()
}
this.logger.flush()
} }
private createLogger(): pino.Logger { private createLogger(): pino.Logger {
if (!isProduction) return pino({ level: 'silent' })
/* 기존 destination 종료 */
if (this.destination) {
this.destination.flushSync()
this.destination.end()
}
/* 새로운 destination 생성 */
this.destination = pino.destination({
dest: getLogFilePath(),
mkdir: true,
sync: false,
})
return pino( return pino(
{ {
level: isProduction ? 'info' : 'silent', level: 'info',
timestamp: pino.stdTimeFunctions.isoTime, timestamp: pino.stdTimeFunctions.isoTime,
}, },
this.destination, this.destination,
@ -57,24 +74,16 @@ class DailyLogger {
} }
public info(obj: any, msg?: string): void { public info(obj: any, msg?: string): void {
const today = new Date().toISOString().split('T')[0] try {
const today: string = getCurrentDate()
if (today !== this.currentDate) { if (today !== this.currentDate) {
/* 기존 destination 종료 */ this.currentDate = today
this.destination.flushSync() this.logger = this.createLogger()
this.destination.end() }
this.logger.info(obj, msg)
/* 새로운 destination 생성 */ } catch (error) {
this.destination = pino.destination({ console.error(`[DailyLogger] Failed to write log: ${error}`)
dest: getLogFilePath(),
mkdir: true,
sync: false,
})
this.currentDate = today
this.logger = this.createLogger()
} }
this.logger.info(obj, msg)
} }
} }
@ -83,6 +92,8 @@ const dailyLogger = new DailyLogger()
/* API 로그 기록 함수 */ /* API 로그 기록 함수 */
export const writeApiLog = async (request: NextRequest, responseStatus: number): Promise<void> => { export const writeApiLog = async (request: NextRequest, responseStatus: number): Promise<void> => {
if (!isProduction) return
const logData: ApiLogData = { const logData: ApiLogData = {
responseStatus: responseStatus, responseStatus: responseStatus,
method: request.method, method: request.method,