중급프로젝트를 마치며

권나현·2025년 8월 14일

[권나현] 개인 개발 보고서

프로젝트명: Dear Carmate – 중고차 계약 관리 서비스
팀명: 1팀
작성자: 권나현
작성일: 2025.08.12

1. 프로젝트 개요

1-1. 프로젝트기간 : 7월 22일 ~ 8월 13일

1-2. 목적:

  • 중고차 판매 과정에서 발생하는 차량·고객·계약 관리 업무를 통합 관리하고, 계약 진행 상황을 시각적으로 확인할 수 있도록 지원하는 서비스입니다.

1-3. 주요 기능:

  • 차량 등록·수정·삭제·조회
  • 유저 관리 (회원가입)
  • 계약 관리 (목록 조회)

2. 주요 개발 내역

백엔드 (Express + Prisma)

  • Prisma ORM을 활용한 DB 모델(차량) 설계 및 마이그레이션
  • 회원가입 API 구현 및 인증 미들웨어 적용
  • 차량 CRUD API 개발
  • 차량 등록 시 type 기본값 설정, 사고 횟수 0 허용 처리
  • 차량 삭제 시 Prisma 트랜잭션으로 연관된 계약 데이터 Soft Delete 처리
  • 차량 제조사/모델(차종) API 구현
  • enum + 매핑 객체 기반 Manufacturer/Model 정의
  • 프론트에서 바로 선택 가능하도록 /cars/models API 제공
  • 계약 목록 상태별 조회 API 개선
  • 계약 상태 필터링 로직 보완
  • contractDraft 상태 추가
  • 차량 검색 기능 개선
  • 차량 번호·차종·차량 ID 기반 조회 기능 구현
  • 유저 프로필 이미지 생성
  • 차량 이미지 생성

프론트엔드/테스트

  • Postman을 이용한 백엔드 API 단위 테스트 및 통합 테스트
  • 프론트 연동 후 차량 등록/수정/조회/삭제, 계약 조회 기능 검증, 유저 회원가입
  • 필드 누락, 타입 불일치, 상태 필터 오류 등 디버깅 및 수정

3. 개발 이슈 및 해결 내역

이슈원인해결 방법
차량 등록 시 사고횟수 0 입력 시 필수값 누락 처리됨0이 falsy 값으로 인식됨data.accidentCount === undefined 조건으로 수정
프론트에서 /cars/models 호출 시 데이터 미노출DB 기반 조회만 구현됨백엔드에서 Manufacturer·Model 매핑 객체 기반 응답 생성
차량 삭제 시 계약 데이터 남음차량 삭제 로직과 계약 삭제 로직이 분리됨Prisma 트랜잭션으로 차량+계약 Soft Delete 동시 처리
차량 검색 시 잘못된 결과 반환ID 조회만 지원차량번호·차종·차량 ID 모두 지원하는 조건문 추가
계약 상태별 조회 누락 (contractDraft)상태 배열에 해당 값 없음상태 배열에 contractDraft 추가
차량 이미지 수정 후 반영 안됨 (imageUrl 업데이트 누락)서비스 레이어에서 imageUrl을 업데이트 패치에 포함하지 않음허용 필드에 imageUrl 추가, '' → null 처리 후 서비스로 전달

4. 성과 및 배운 점

  • Prisma 트랜잭션을 활용한 연관 데이터 처리 경험을 하였다.
  • 타입스크립트에서 enum + 매핑 객체를 사용하여 구조적 데이터 관리 방법 습득하였다.
  • 프론트와 백엔드 API 스펙 불일치 시, 백엔드에서 대응하는 방안 설계를 경험 하였다.
  • 필수값 검증 로직에서 0 값 허용 처리와 같은 예외 케이스 대응 역량 향상 되었다.
  • 상태 필터링, 조건부 검색 등 동적 쿼리 작성 경험 하였다.

5. 향후 과제 및 개선 방향

  • 차량/계약 데이터 삭제 시 완전 삭제 옵션 추가
  • 계약 등록 시 기본 미팅 일정 자동 생성 로직 구현
  • 차량 모델 데이터 DB 테이블화 및 관리자 페이지에서 직접 관리 가능하게 개선
  • 테스트 코드(Jest) 작성으로 기능 안정성 확보

6. 구체적 개발 예시

  • 차량 삭제 시 계약 동시 삭제 (Prisma 트랜잭션)
export const deleteCarCascade = async (carId: bigint, companyId: bigint) => {
  return prisma.$transaction(async (tx) => {
    // 1) 차량 소유/존재 확인
    const car = await tx.car.findFirst({
      where: { id: carId, companyId, isDeleted: false },
      select: { id: true },
    });
    if (!car) throw new NotFoundError('존재하지 않거나 이미 삭제된 차량입니다.');

    // 계약 문서 삭제
    await tx.contractDocument.deleteMany({
      where: { contract: { carId } },
    });

    // 미팅 알람 삭제
    await tx.alarm.deleteMany({
      where: { meeting: { contract: { carId } } },
    });

    // 미팅 삭제
    await tx.meeting.deleteMany({
      where: { contract: { carId } },
    });

    // 계약 소프트 삭제
    await tx.contract.updateMany({
      where: { carId },
      data: { isDeleted: true, deletedAt: new Date() },
    });

    // 차량 이미지 실제 삭제
    await tx.carImage.deleteMany({
      where: { carId },
    });

    // 차량 소프트 삭제
    await tx.car.update({
      where: { id: carId },
      data: { isDeleted: true, deletedAt: new Date() },
    });

    return { id: Number(carId) };
  });
};
  • 사고 횟수 0 허용 처리
if (
  !data.carNumber ||
  !data.manufacturer ||
  !data.model ||
  !data.type ||
  data.price === undefined || data.price === null ||
  data.accidentCount === undefined || data.accidentCount === null
) {
  return res.status(400).json({ message: '필수 값이 누락되었습니다.' });
}
스크린샷 2025-08-12 092629
  • 차량 모델/제조사 매핑 응답
export const getCarModels = (_req: Request, res: Response) => {
  const manufacturers: Manufacturer[] = Object.keys(manufacturerModels) as Manufacturer[];
  const result = manufacturers.map((m) => ({
    manufacturer: m,
    models: manufacturerModels[m],
  }));
  res.status(200).json({ data: result });
};
스크린샷 2025-08-08 162801 스크린샷 2025-08-08 162807
  • 차량 ImageUrl 추가
car.controller.ts
 // imageUrl: 빈문자면 null, 값 있으면 그대로
    if ('imageUrl' in data) {
      if (typeof data.imageUrl === 'string') {
        const t = data.imageUrl.trim();
        data.imageUrl = t === '' ? null : t;
      } else if (data.imageUrl == null) {
        data.imageUrl = null;
      }
    }

car.service.ts
 // ★ 핵심: imageUrl 반영 (빈 문자열이면 null, 값 있으면 그대로)
  if ('imageUrl' in (data || {})) {
    patch.imageUrl = toTrimmedOrNull(data.imageUrl);
  }
스크린샷 2025-08-13 002453 스크린샷 2025-08-13 002443 스크린샷 2025-08-14 183502

7. 소감

끝나지 않을 것만 같던 중급 프로젝트가 드디어 마무리됐다.
비록 공식 기간은 짧았지만, 체감상 한 달 넘게 온 힘을 쏟아부은 듯하다.

이번 프로젝트를 하면서 가장 크게 느낀 건 팀워크의 중요성과 자기 객관화였다.
내가 가진 역량이 어느 정도인지 명확히 알아야 하고, 부족한 부분이 있다면 어떻게든 채우고 맞춰가는 노력이 필요하다는 걸 다시금 깨달았다. 처음엔 금방 끝낼 수 있을 거라 생각했지만, 진행 속도가 늦춰지고 의견이 엇갈리는 상황을 겪으며, 모든 조건을 완벽히 맞춘다는 게 얼마나 어려운 일인지 실감했다.

그럼에도 불구하고, 끝까지 잘 따라오지 못하는 팀원도 놓치지 않고 함께 이끌어가며 프로젝트를 마무리할 수 있었다는 점이 가장 뿌듯하다. 결과적으로, 여러 어려움 속에서도 팀 전체가 함께 완주했고, 성공적으로 프로젝트를 마쳤다는 사실이 큰 보람으로 남는다.

profile
node.js 백앤드 개발자가 되기 위한 Study Vlog

0개의 댓글