1. 프로젝트 개요
- 공통 목적
각 기능 별 로직 및 코드 이해, 실력 향상 - 핵심 기능
유저 인증·인가, 차량 및 고객 등 ENUM을 활용한 CRUD, 대용량 업로드,
등록된 차량 및 고객을 활용한 계약 및 상태 구현, 대시보드
2. 담당한 작업
고객
- 컨트롤러 설계 및 구현
Express와 인증 미들웨어(verifyAccessToken.ts)를 활용해 고객 CRUD, 검색, 상세조회, 대용량업로드 작성 - DTO 정의 및 입력 변환 작성
Create/UpdateCustomerDTO 인터페이스를 선언, 입력값을 Prisma enum으로 바꿔주는 convertInput 유틸 구현 - Enum 변환 로직
한글·영문 라벨을 성별(Gender), 연령대(AgeGroup), 지역(Region) enum으로 매핑하는 toEnum 함수를 작성 - 데이터 매퍼 구현
DB에서 가져온 Customer 객체를 소문자·null 처리 및 한글 라벨 필드가 포함된 응답 형태로 변환하는
toLabeledCustomer 작성 - 서비스 레이어 작성
존재 검사, 예외 처리와 CSV 파싱·대용량 등록 기능을 담당하며, Prisma 트랜잭션 및 예외 쓰로우 구현 - 레포지토리 레이어 구축
Prisma Client를 통해 고객 CRUD, 검색 메서드와 페이징·트랜잭션 처리를 담당하는 함수들을 구현했습니다.
대시보드
- 컨트롤러 구현
getDashboardStatsHandler에서 인증된 companyId를 뽑아 dashboardService.getDashboardStats를 호출 - 서비스 집계 로직 작성
Prisma의 aggregate와 groupBy를 이용해 이달·지난달 매출 합계, 진행 중·완료된 계약 건수를 병렬 조회 - 차종별 매핑 및 그룹핑
car 테이블에서 모델 타입을 조회해, 계약·매출 데이터를 carId 기준으로 차종별 카운트·매출 합계로 재분류 - 성장률 계산
이번 달 매출과 지난 달 매출을 비교해 백분율로 성장률을 산출, 과거 대비 증감치 확인 - 응답 데이터 포맷팅
내부 객체(contractsByCarType, salesByCarType)를 { carType, count } 배열 형태로 변환(formatGroup)하여 클라이언트가 바로 사용할 수 있게 정리
3. 기술적 성과
기술 스택
- 언어
TypeScript - 데이터베이스·ORM
PostgreSQL, Prisma ORM - 인증·인가
JWT, express-jwt 미들웨어, Redis - 파일 처리
multer, csv-parse, csv-parser - 입력 검증·타입 안정성
Superstruct, TypeScript DTO & Enum 매퍼 - 에러 핸들링
커스텀 에러 클래스(BadRequest, NotFound, Unauthorized 등), 글로벌 에러 미들웨어 - 배포·테스트
Render.com 배포, Postman 테스트 컬렉션, RestClient
4. 문제점 및 해결 과정
- DB 히트 감소
- customerService.ts
기존 이런 식으로 코드를 작성하였으나 위와 같은 방식을 사용하면 부하가 증가한다고 하여 아래와 같이 수정을 해보았다.
- customerService.ts
[ ... ]
위와 같이 하지 않는 이유
export const prisma = new PrismaClient();로 같은 이름으로 다시 선언하려 해서 충돌 발생
또한 기존 컨트롤러 내에 삭제에 해당하는 부분에서
if (result.count === 0) {
throw new NotFoundError('존재하지 않는 고객입니다');
}
res.status(200).json({ message: '고객 삭제 성공' });
};
해당 부분이 서비스에 들어가는게 더 적합한것으로 판단,
따라서 아래 내용을
- customerController.ts
- customerService.ts
아래와 같이 수정
(컨트롤러에 있던 내용을 서비스로 이동)
- customerController.ts
- customerService.ts
- 대용량 업데이트 버그 수정
업로드는 한 것 같은데 DB에 내용이 적용이 안되는 것을 식별함
- customerController.ts
- toLowerCaseCustomer 함수에서 gender의 기본값을 null → ' ' 빈 문자열로 변경
- bulkUploadCustomer 핸들러가 버퍼(buffer) 대신 req.file.path를 서비스로 넘기도록 수정
- 기존: if (!fileBuffer) …, bulkUploadCustomers(companyId, fileBuffer)
- 변경: if (!req.file?.path) …, bulkUploadCustomers(companyId, req.file.path)

수정 중 필수로 들어가는 값인 gender에 null이 사용 된 것을 식별
따라서 위에서 설명한 것 처럼 빈 문자열을 주는 것으로 대체
- customerService.ts
- bulkUploadCustomers 시그니처를 buffer: Buffer → filePath: string으로 변경
- 내부에서 fs.readFileSync(filePath, 'utf-8')으로 파일을 읽고, BOM(\uFEFF) 제거 후 파싱
- CSV 레코드를 맵핑한 뒤 name·gender 필터링 로직 추가해 빈 값은 자동 스킵
- createMany 실행 결과(result.count)를 활용해 “성공적으로 N명의 고객을 업로드했습니다”라는 메시지 반환

이 변경으로 버퍼가 비어 있을 때 발생하던 오류를 피할 수 있고, 파일 경로 기반 처리를 통해 Multer 업로드 안정성을 높였습니다. 또한, 업로드한 고객 수를 결과 메시지에 반영해 테스트 과정에서 정상적으로 업데이트 되었는지 확인했습니다.
- 고객 생성 버그 수정
- customerController.ts 리팩토링
- 요청 바디에서 gender, ageGroup, region 레이블을 분리해 toGenderEnum, toAgeGroupEnum, toRegionEnum 유틸로 Prisma enum 변환 로직을 이동
- 응답용 매핑 함수 toLowerCaseCustomer를 외부 customerMapper 모듈로 분리·import

- customerEnumConverter.ts 신규 추가
- 한국어(남성, 여성, 10대, 20대 등등 …) 및 영어 레이블을 Prisma enum(Gender, AgeGroup, Region)으로 변환하는 toGenderEnum, toAgeGroupEnum, toRegionEnum 함수 정의
- customerMapper.ts 신규 추가
- DB에서 조회한 Customer 객체의 문자열 필드(gender, ageGroup, region)를 소문자(toLowerCase())로 변환해 반환하는 toLowerCaseCustomer 함수
- customerService.ts 단순화
- 컨트롤러로 역할이 이동된 변환 로직(유틸 호출·CSV 파싱 등)을 제거
- CreateCustomerDTO를 그대로 받아 Prisma.CustomerUncheckedCreateInput으로 매핑하는 책임만 수행하도록 축소
서비스 밑에 있던 내용 EnumConverter로 옮김
- customerType.ts 정리
- 기존 변환 함수들을 정리·이관하고, 응답용 타입 CustomerForResponse 정의를 유지·정비
- 프론트와 네이밍 불일치 문제 해결
- 주요사항
- 아래 첨부 이미지와 같이 성별, 연령대, 지역이 영어로 출력 되던 것을 한글로 출력되도록 수정
- 프론트 수정 내용
1. 네이밍
const processedRecord = { ...record, gender:
CUSTOMER_GENDER_MAP[record.gender], ageGroup: record.ageGroup || '-',
region: record.region || '-', }
위 내용을 아래와 같이 수정
const processedRecord = { ...record, gender: record.genderToLabel || "-",
ageGroup: record.ageGroupToLabel || "-", region: record.regionToLabel || "-",
};
2. 임포트 밑부분 내용 수정 타입 지정
type CustomersInfoTableProps = { data: CustomerType[] }
위 내용을 아래와 같이 수정
type CustomerWithLabel = CustomerType & {
genderToLabel?: string;
ageGroupToLabel?: string | null;
regionToLabel?: string | null;
};
type CustomersInfoTableProps = {
data: CustomerWithLabel[];
};
- 프론트 관련 수정 이미지 첨부
1. 기존

1. 수정

2. 기존

2. 수정

- 고객 수정이 안되던 버그 수정
- 문제 요인
- 프론트에서 gender, ageGroup, region 값을 한글("남성", "30대", "서울")로 보냄
- 백엔드는 이 값을 enum 타입인 영어 MALE, THIRTIES, SEOUL 로 변환하여 DB에 저장
- 하지만 이미 저장된 데이터를 수정할 때, 프론트에서 함께 보내는 genderToLabel, ageGroupToLabel 등의 표시용 필드가 Prisma.customer.update에 포함되어 스키마에 없는 필드 오류 발생
- 해결 방안
- updateCustomer에서 genderToLabel 등의 비DB 필드들을 제거
- gender, ageGroup, region에 대해서는 한글이면 영어 enum으로 변환한 후 저장
- 불필요한 필드를 제거하고 변환처리 추가
수정 이미지 첨부

...

5. 협업 및 피드백
아래 첨부 이미지처럼 PR리뷰를 주로 진행하고, 풀리지 않는 문제나 코드를 작성하던 중 떠오르는 궁금증은 즉시 팀원과 공유하거나 의논하여 토의 및 검색을 통한 문제 해결을 진행하였습니다.
- 첨부 이미지



6. 코드 품질 및 최적화
customerController.ts
- 기존 형태
export const createCustomer = async (req: AuthenticatedRequest, res: Response) => {
const companyId = (req.user as { companyId: number }).companyId;
const { gender, ageGroup, region, ...rest } = req.body;
const converted = {
...rest,
...(gender && { gender: toGenderEnum(gender) }),
...(ageGroup && { ageGroup: toAgeGroupEnum(ageGroup) }),
...(region && { region: toRegionEnum(region) }),
};
[...]
};
export const updateCustomer = async (req: AuthenticatedRequest, res: Response) => {
const companyId = (req.user as { companyId: number }).companyId;
const customerId = Number(req.params.id);
const { gender, ageGroup, region, ...rest } = req.body;
const convertedData = {
...rest,
gender: gender ? toGenderEnum(gender) : undefined,
ageGroup: ageGroup ? toAgeGroupEnum(ageGroup) : undefined,
region: region ? toRegionEnum(region) : undefined,
};
[...]
};
위와 같이 toGenderEnum, toAgeGroupEnum, toRegionEnum 을 매번 컨트롤러에서 호출 이를 중복이라고 판단. 가독성과 의도 파악이 어려움.
따라서 유틸로 통합하고 반복을 제거 할 것.
customerInputConverter.ts 생성
- customerInputConverter.ts로 중복 내용 분리 (중복 제거 - 핵심)
import { CreateCustomerDTO, UpdateCustomerDTO } from '../../../dto/customer.dto';
import { toGenderEnum, toAgeGroupEnum, toRegionEnum } from './customerEnumConverter';
export function convertCreateCustomerInput(input: unknown): CreateCustomerDTO {
const { name, gender, phoneNumber, ageGroup, region, email, memo } = input as Record<
string,
string
>;
return {
name,
gender: toGenderEnum(gender),
phoneNumber,
ageGroup: ageGroup ? toAgeGroupEnum(ageGroup) : undefined,
region: region ? toRegionEnum(region) : undefined,
email,
memo,
};
}
export function convertUpdateCustomerInput(input: unknown): UpdateCustomerDTO {
const { name, gender, phoneNumber, ageGroup, region, email, memo } = input as Record<
string,
string
>;
return {
name,
gender: toGenderEnum(gender),
phoneNumber,
ageGroup: ageGroup ? toAgeGroupEnum(ageGroup) : undefined,
region: region ? toRegionEnum(region) : undefined,
email,
memo,
};
}
- gender, ageGroup, region을 함수로 추출해 유지 보수를 쉽게 하기 위함
- convertCreateCustomerInput라는 함수 명을 통해 ‘이 함수는 고객 등록할 때 입력 값을 변환하는 용도다’ 라는 목적을 명시 (의도 명확화)
- 컨트롤러에서의 변환 로직이 분리되면서 핵심 흐름만 드러나게 함. (가독성)
- 수정된 형태
export const createCustomer = async (req: AuthenticatedRequest, res: Response) => {
const companyId = (req.user as { companyId: number }).companyId;
const converted = convertCreateCustomerInput(req.body);
[...]
};
export const updateCustomer = async (req: AuthenticatedRequest, res: Response) => {
const companyId = (req.user as { companyId: number }).companyId;
const customerId = Number(req.params.id);
const converted = convertUpdateCustomerInput(req.body);
const updated = await customerService.updateCustomer(customerId, companyId, converted);
[...]
res.status(200).json(toLabeledCustomer(updated));
};
중간 점검✔ : 1차 수정 후 정상 작동 확인
customerService.ts
- 기존 형태 ( 생성 부분 )
export const createCustomer = (companyId: number, data: CreateCustomerDTO) => {
const prismaData: Prisma.CustomerUncheckedCreateInput = {
...data,
companyId,
gender: toGenderEnum(data.gender),
ageGroup: toAgeGroupEnum(data.ageGroup),
region: toRegionEnum(data.region),
};
return customerRepo.createCustomer(companyId, prismaData);
};
위 코드에서는 enum 변환 로직을 제거 함
이미 컨트롤러에서 처리하고 있기 때문
따라서 아래와 같이 수정
- 수정된 형태 ( 생성 부분 )
export const createCustomer = (companyId: number, data: CreateCustomerDTO) => {
const prismaData: Prisma.CustomerUncheckedCreateInput = {
...data,
companyId,
};
return customerRepo.createCustomer(companyId, prismaData);
};
- 기존 형태 ( 수정 부분 )
export const updateCustomer = async (
customerId: number,
companyId: number,
data: UpdateCustomerDTO,
) => {
const customer = await customerRepo.getCustomerById(customerId, companyId);
if (!customer) throw new NotFoundError('고객을 찾을 수 없습니다.');
const { genderToLabel, ageGroupToLabel, regionToLabel, ...cleanData } = data as any;
const converted: Prisma.CustomerUncheckedUpdateManyInput = {
...cleanData,
...(cleanData.gender && { gender: toGenderEnum(cleanData.gender) }),
...(cleanData.ageGroup && { ageGroup: toAgeGroupEnum(cleanData.ageGroup) }),
...(cleanData.region && { region: toRegionEnum(cleanData.region) }),
};
return customerRepo.updateCustomer(customerId, companyId, converted);
};
고객 정보 수정에서도 마찬가지로 enum 변환 로직을 제거 함
사유는 동일함
- 수정된 형태 ( 수정 부분 )
export const updateCustomer = async (
customerId: number,
companyId: number,
data: UpdateCustomerDTO,
) => {
const customer = await customerRepo.getCustomerById(customerId, companyId);
if (!customer) throw new NotFoundError('고객을 찾을 수 없습니다.');
const { genderToLabel, ageGroupToLabel, regionToLabel, ...cleanData } = data as any;
const converted: Prisma.CustomerUncheckedUpdateManyInput = {
...cleanData,
};
return customerRepo.updateCustomer(customerId, companyId, converted);
};
중간 점검✔ : 2차 수정 후 정상 작동 확인
0509 04:45 최종 점검✔: 정상 작동 확인
핵심 목표
- 중복 제거
- 의도 명확화
- 가독성
7. 향후 개선 사항 및 제안
당장 떠오르는 것들 ..
- any 완전히 없애기, null 극 최소화
- 트랜잭션 처리 더 추가해보기
- 검증강화
- jwt 토큰 블랙아웃등 로그아웃 처리나 refreshToken rotation을 통한 정보 탈취 방지
'프로젝트 회고' 카테고리의 다른 글
1차 팀프로젝트 Style part 개별 보고서 (1) | 2025.03.21 |
---|