File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
/**
* 게시판(공지사항·1:1문의·FAQ) 도메인 타입 — 순수 데이터 표현, 외부 의존 없음.
*
* 세 화면은 **백엔드에서 같은 테이블(TB_COM_BBS)·같은 API**를 쓰고 `stngId`(게시판 설정 ID)로만
* 갈린다. 그래서 타입과 코드 표는 여기 한 곳에 모으고, 화면별로 다른 것(검색 조건·정렬·컬럼)만
* 각자의 query 파일에서 다룬다.
*
* 데이터 출처: `GET /api/v1/mngr/bbs/pagination` (edupay-backend develop 924db37)
*
* **`null`의 의미는 "백엔드가 아직 주지 않는 항목"이다.** 노출기간·앱푸쉬는 등록/수정으로 저장은
* 되지만 조회 SQL의 select 목록에 빠져 있어 다시 읽으면 항상 비어 있다(사용자 확인 후 "입력은
* 만들고 전송하되 조회는 빈 값" 방침). 백엔드가 select에 컬럼을 추가하면 Repository 매핑만
* 늘리면 값이 그대로 채워진다.
*/
/** 세 화면을 구분하는 값 — 라우트·API 파라미터·코드 표의 키를 겸한다. */
export type BoardType = 'notice' | 'inquiry' | 'faq';
/**
* TODO: 추후 메뉴 데이터에 실려 오면 이 상수를 지우고 그 값을 쓴다.
* `GET /api/v1/common/bbs/stng/list`의 실제 값(typeSe는 셋 다 'NOT'이라 구분에 쓸 수 없다).
*/
export const BOARD_SETTING_IDS: Record<BoardType, string> = {
notice: '5e9ae040df1b41c1b03ce11d1bafcc60',
inquiry: '4538fa2b88cc458da5cae972f39a1ebd',
faq: '9f3d4c7b2a3e4b7d8c1f6a9e2b5c7d10',
};
export type BoardPost = {
/** 백엔드 `bbsId`. 행의 key이자 단건 조회·수정·삭제의 입력값이다. */
id: string;
/** 제목 — 백엔드 `bbsNm`. */
title: string;
/** 본문 — 백엔드 `bbsCn`. */
content: string;
/** 구분 코드 — 백엔드 `bbsCd`. */
categoryCode: string;
/**
* 유형 코드(FAQ의 유형 / 1:1문의의 질문유형).
* **백엔드에 아직 저장 필드가 없다** — `BOARD_TYPE_CODE_PARAM` 주석 참조. 조회 시 항상 null이다.
*/
typeCode: string | null;
/** 첨부파일 식별자 — 백엔드 `atchFileId`. 파일명·용량 조회 API가 없어 존재 여부만 알 수 있다. */
attachmentId: string | null;
/** 상단고정 — 백엔드 `hghrkYn`. */
isPinned: boolean | null;
/** 사용여부(노출여부) — 백엔드 `rlsYn`. */
isVisible: boolean | null;
/** 노출 시작일. 백엔드 조회 응답에 없어 현재는 항상 null이다(파일 상단 주석). */
startDate: string | null;
/** 노출 종료일. 위와 같다. */
endDate: string | null;
/** 앱푸쉬 발송 여부. 위와 같다. */
isPushEnabled: boolean | null;
/** 작성자 표기 — 백엔드 `regNm`(이름(로그인ID) 형태로 이미 조합돼 온다). */
authorName: string | null;
/** 작성일(YYYY-MM-DD) — 백엔드 `regDt`. */
createdAt: string | null;
/** 조회수 — 백엔드 `inqCnt`. */
viewCount: number | null;
/* ── 1:1문의 전용 ── */
/** 질문자 연락처 — 백엔드 `telNo`. */
authorPhoneNumber: string | null;
/** 질문자 이메일 — 백엔드 `emlAddr`. */
authorEmail: string | null;
/** 답변 내용 — 백엔드 `ansCn`. */
answerContent: string | null;
/** 답변일 — 백엔드 `ansDt`. */
answeredAt: string | null;
/** 답변자 표기 — 백엔드 `answrNm`. */
answererName: string | null;
/** 진행상태 코드 — 백엔드 `ansSttsCd`. */
answerStatusCode: string | null;
};
/** 값이 없는 항목의 화면 표기. 표·팝업이 같은 문자를 쓰도록 여기 한 곳에 둔다. */
export const EMPTY_FIELD_PLACEHOLDER = '-';
export function formatOptionalValue(value: string | number | null): string {
return value === null || value === '' ? EMPTY_FIELD_PLACEHOLDER : String(value);
}
/**
* 「유형」을 실어 보낼 요청 파라미터 이름 — **임시 이름이다.**
*
* 시안은 구분과 유형을 별도 항목으로 요구하지만 백엔드에는 `BBS_CD` 하나뿐이고, 등록/수정 요청
* VO(`MngrBbsRequestVo`)에는 그마저도 없다. 사용자 확인 결과 **"백엔드가 필드를 추가할 예정이니
* 프론트는 미리 구현"** 이라, 구분은 `bbsCd`로 유형은 이 이름으로 보낸다.
*
* 백엔드가 실제 이름을 확정하면 이 상수 한 줄만 고치면 된다. 그전까지 백엔드는 이 파라미터를
* 무시하므로 **유형은 저장되지 않는다**(화면 입력과 전송은 정상 동작한다).
*/
export const BOARD_TYPE_CODE_PARAM = 'bbsTypeCd';
type CodeOption = { value: string; label: string };
/**
* 「구분」 코드 표 — **임시값이다.** 백엔드에 코드 상수도 코드테이블도 없어 시안의 라벨을 기준으로
* 잠정 코드를 붙였다. 실제 코드가 확정되면 이 표만 교체한다.
*
* 시안의 공지사항 구분은 "공통 / 메뉴명1, 메뉴명2…"이고 FAQ는 "공통 / FOX PAY"다. 확정되지 않은
* "메뉴명N"은 임의로 지어내지 않고 두 화면이 공통으로 쓰는 값만 둔다.
*/
export const BOARD_CATEGORY_OPTIONS: readonly CodeOption[] = [
{ value: 'COMMON', label: '공통' },
{ value: 'FOXPAY', label: 'FOX PAY' },
];
/** 1:1문의의 「질문유형」 — 시안 A_BOA_012. 잠정값이다. */
export const INQUIRY_TYPE_OPTIONS: readonly CodeOption[] = [
{ value: 'SIGNUP', label: '회원가입/로그인' },
{ value: 'ERROR', label: '오류신고' },
{ value: 'ETC', label: '기타' },
];
/**
* 1:1문의 「진행상태」(`ansSttsCd`) — **임시값이다.** 백엔드에 코드 정의가 없어 사용자 확인 후
* 잠정 코드를 한 곳에 모아 두는 방침으로 정했다. 실제 코드가 나오면 이 표만 교체한다.
*/
// TODO: 답변완료를 가리키는 코드값. BBS_ANS_CD의 실제 값이 확인되면 교체한다.
export const ANSWER_STATUS_DONE = 'DONE';
/** 코드 → 라벨. 표에 없는 코드는 지어내지 않고 코드 그대로 노출한다(잘못된 라벨보다 낫다). */
function formatCode(options: readonly CodeOption[], code: string | null): string {
if (code === null || code === '') {
return EMPTY_FIELD_PLACEHOLDER;
}
return options.find((option) => option.value === code)?.label ?? code;
}
export function formatBoardCategoryLabel(code: string | null): string {
return formatCode(BOARD_CATEGORY_OPTIONS, code);
}
/** FAQ의 「유형」 — 공통코드 `BBS_FAQ_CD`. 코드표는 호출부가 조회해 넘긴다. */
export function formatFaqTypeLabel(
code: string | null,
options: readonly CodeOption[]
): string {
return formatCode(options, code);
}
export function formatInquiryTypeLabel(code: string | null): string {
return formatCode(INQUIRY_TYPE_OPTIONS, code);
}
export function formatAnswerStatusLabel(
code: string | null,
options: readonly CodeOption[]
): string {
return formatCode(options, code);
}
/** 사용여부 표기 — 시안의 「노출 / 미노출」. */
export function formatVisibilityLabel(isVisible: boolean | null): string {
if (isVisible === null) {
return EMPTY_FIELD_PLACEHOLDER;
}
return isVisible ? '노출' : '미노출';
}
/**
* 1:1문의 목록의 전화번호 마스킹(시안 A_BOA_012 — `010-12**-**78`).
*
* 사용자 확인 후 **적용**하기로 한 항목이다(학생 회원 목록은 반대로 "마스킹 없음"이 지시였다 —
* 화면마다 방침이 다르므로 이 함수를 1:1문의에서만 쓴다).
*
* 가운데 자리는 앞 2자리만, 끝자리는 뒤 2자리만 남긴다. 형식이 `nnn-nnnn-nnnn`이 아니면 마스킹
* 규칙을 적용할 자리를 알 수 없으므로 **전부 가린다** — 규칙 밖의 값을 원본 그대로 흘리는 것이
* 마스킹의 목적에 어긋나기 때문이다.
*/
export function maskPhoneNumber(phoneNumber: string | null): string {
if (phoneNumber === null || phoneNumber === '') {
return EMPTY_FIELD_PLACEHOLDER;
}
const parts = phoneNumber.split('-');
if (parts.length !== 3) {
return '***';
}
const [prefix, middle, last] = parts;
const maskedMiddle = middle.slice(0, 2).padEnd(middle.length, '*');
const maskedLast = '*'.repeat(Math.max(0, last.length - 2)) + last.slice(-2);
return `${prefix}-${maskedMiddle}-${maskedLast}`;
}