Merge branch 'iipbs/sn/dev' into iipbs/dm/dev

This commit is contained in:
2026-03-27 14:36:57 +09:00
69 changed files with 8725 additions and 362 deletions
+38
View File
@@ -0,0 +1,38 @@
## Project
This repository contains a Next.js web application.
Main technologies:
- Next.js
- TypeScript
- pnpm workspace
- turborepo
이 저장소는 pnpm workspace + Turborepo 기반 monorepo이다.
- 워크스페이스 범위는 `apps/*`, `apps/common/*`, `packages/*`, `packages/common/*`
- 루트 스크립트는 `turbo run build`, `turbo run dev` 를 사용한다
- `build` 작업은 의존 패키지의 `build` 를 먼저 수행한다 (`dependsOn: ["^build"]`)
- `dev` 는 캐시하지 않는 지속 실행 작업이다
- 변경 범위만 선택 실행할 수 있으므로, 전체 빌드보다 `--filter` 사용을 우선한다
## Encoding
- UTF-8
- 한글이 깨지지 않도록 주의할 것
## 주석 comment
- 한글 주석을 삭제하지 말 것
## Monorepo structure
apps/
_template
as
cm
cs
dc
dm
dm_new
fo
sn
st
wr
@@ -48,6 +48,16 @@ type SuggestionSeed = {
bldgNoMno: number;
};
/**
* 주소 검색 API를 호출해 우편번호/주소 목록을 조회한다.
* @param searchQuery 조회 조건 객체
* @param searchQuery.roadNm 도로명
* @param searchQuery.bldgNoMno 건물 본번
* @param searchQuery.bldgNoSno 건물 부번
* @param searchQuery.page 0부터 시작하는 페이지 번호
* @param searchQuery.size 페이지당 조회 건수
* @returns 주소 목록과 전체 건수를 포함한 응답 객체
*/
async function findAddress(searchQuery: ZipSearchParams): Promise<ZipListResponse> {
if (!searchQuery) return {};
@@ -68,6 +78,12 @@ async function findAddress(searchQuery: ZipSearchParams): Promise<ZipListRespons
return (await response.json()) as ZipListResponse;
}
/**
* 자동완성 후보를 만들기 위해 주소 항목에서 도로명과 건물 본번을 추출한다.
* 우선 API 응답 필드의 값을 사용하고, 값이 없으면 표시 주소 문자열을 파싱해 보정한다.
* @param item 주소 검색 결과 1건
* @returns 자동완성 생성에 사용할 도로명/건물본번 또는 추출 실패 시 null
*/
function extractSuggestionSeed(item: Zip): SuggestionSeed | null {
const ext = item as Zip & {
roadNm?: string;
@@ -97,6 +113,11 @@ function extractSuggestionSeed(item: Zip): SuggestionSeed | null {
return { roadNm, bldgNoMno };
}
/**
* 검색어에 허용되지 않는 특수문자나 SQL 예약어가 포함되어 있는지 검사한다.
* @param search 사용자가 입력한 검색 문자열
* @returns 검색 가능하면 true, 차단 대상이면 false
*/
function searchFilter(search: string): boolean {
if (search.length === 0) return true;
@@ -132,6 +153,13 @@ function searchFilter(search: string): boolean {
return true;
}
/**
* 단일 입력 주소 문자열을 "도로명 + 건물번호" 구조로 파싱해 API 조회 파라미터로 변환한다.
* 예) "테헤란로 152-1" -> { roadNm: "테헤란로", bldgNoMno: 152, bldgNoSno: 1, page }
* @param input 사용자가 입력한 주소 검색어
* @param page 1부터 시작하는 요청 페이지 번호
* @returns 파싱 성공 시 API 조회 파라미터와 정규화된 검색어, 실패 시 오류 메시지
*/
export function parsePostcodeInput(
input: string,
page = 1,
@@ -205,6 +233,12 @@ export function parsePostcodeInput(
};
}
/**
* 전체 도로명 주소 문자열에서 재검색에 사용할 "도로명 건물번호" 입력값만 추출한다.
* 예) "테헤란로 152, OO빌딩" -> "테헤란로 152"
* @param roadAddr 화면에 표시된 도로명 주소 문자열
* @returns 재검색 입력란에 넣을 수 있는 문자열, 추출 실패 시 빈 문자열
*/
export function extractSearchInputFromRoadAddr(roadAddr: string): string {
const firstSegment = roadAddr.split(",")[0]?.trim() ?? "";
if (!firstSegment) return "";
@@ -216,6 +250,12 @@ export function extractSearchInputFromRoadAddr(roadAddr: string): string {
return `${parts.slice(0, -1).join(" ")} ${bldgToken}`.trim();
}
/**
* 사용자가 입력 중인 도로명을 기준으로 자동완성 후보를 조회한다.
* 건물번호가 함께 입력된 경우에는 마지막 숫자 토큰을 제외한 도로명만으로 후보를 찾는다.
* @param input 사용자가 입력창에 입력한 원본 문자열
* @returns 최대 5개의 "도로명 건물본번" 자동완성 문자열 목록
*/
export async function fetchPostcodeSuggestions(input: string): Promise<string[]> {
const normalized = input.replace(/\s+/g, " ").trim();
const parts = normalized.split(" ");
@@ -252,6 +292,15 @@ export async function fetchPostcodeSuggestions(input: string): Promise<string[]>
return suggestions;
}
/**
* 우편번호 검색 API를 호출해 화면 표시용 우편번호 목록 형태로 변환한다.
* @param params 조회 조건 객체
* @param params.roadNm 검색할 도로명
* @param params.bldgNoMno 건물 본번
* @param params.bldgNoSno 건물 부번
* @param params.page 1부터 시작하는 화면 페이지 번호
* @returns 전체 건수와 화면 표시용 주소 목록
*/
export async function fetchPostcode(
params: PostcodeSearchParams,
): Promise<PostcodeSearchResult> {