카드형식 물물교환 플랫폼 | KH정보교육원 Pclass오후반 2조 파이널 프로젝트
2021.11.15 ~ 2021.12.10 (총 25일) | 팀 6명
코로나19 장기화로 오프라인 소비가 크게 감소하고, MZ세대를 중심으로 온라인 중고거래 시장이 급성장하는 트렌드에 주목하였습니다.
단순 중고거래를 넘어, 카드 형식으로 물건을 등록하고 원하는 카드와 교환하는 물물교환 플랫폼을 기획·개발하였습니다.
게임 내 교환 시스템 UI에서 영감을 받아 MZ세대에게 친숙하고 직관적인 인터페이스를 구현하였으며,
불필요한 소비를 줄이고 생활폐기물 감소에도 기여할 수 있는 서비스를 목표로 하였습니다.
| 항목 | 내용 |
|---|---|
| 프로젝트명 | SwitchSwitch |
| 개발 기간 | 2021.11.15 ~ 2021.12.10 (총 25일) |
| 팀 구성 | 6명 (이민지, 김현진, 송국희, 정현직, 정영운, 홍용천) |
| GitHub | https://github.com/minjeeleee/final-switch |
| 서버 | Apache Tomcat 8.5 / Oracle Cloud DB |
| OS | Windows 10 |
| API | 버전 | 용도 |
|---|---|---|
| Kakao Developers | - | 소셜 로그인, 주소 검색 |
| 금융결제원 API | - | 계좌 실명인증·계좌인증 |
| 아임포트(iamport) | 0.2.17 | 포인트 결제 처리 |
| Javax.mail | 1.6.2 | 회원가입 이메일 인증 |
final-switch/
├── src/main/
│ ├── java/com/kh/switchswitch/
│ │ ├── admin/ # 관리자
│ │ ├── alarm/ # 실시간 알림
│ │ ├── board/ # 커뮤니티 게시판
│ │ │ ├── controller/
│ │ │ │ └── BoardController.java
│ │ │ └── model/
│ │ │ ├── dto/
│ │ │ │ ├── Board.java
│ │ │ │ └── Reply.java
│ │ │ ├── repository/
│ │ │ │ └── BoardRepository.java # MyBatis @Mapper
│ │ │ └── service/
│ │ │ ├── BoardService.java
│ │ │ └── BoardServiceImpl.java
│ │ ├── card/ # 카드 등록·관리
│ │ ├── chat/ # 실시간 채팅
│ │ ├── common/ # 공통 모듈
│ │ │ ├── code/
│ │ │ │ ├── Config.java # 도메인·업로드 경로·이메일 설정 (Enum)
│ │ │ │ └── ErrorCode.java # 전역 에러코드 정의 (Enum)
│ │ │ ├── config/
│ │ │ │ ├── SecurityConfig.java # Spring Security 설정
│ │ │ │ ├── ThymeleafConfig.java
│ │ │ │ └── WebSocketConfig.java
│ │ │ ├── exception/
│ │ │ │ ├── HandlableException.java # 커스텀 예외
│ │ │ │ └── handler/ExceptionAdvice.java # @ControllerAdvice 전역 예외처리
│ │ │ ├── mail/ # 이메일 인증
│ │ │ ├── security/ # 로그인 성공·실패 핸들러
│ │ │ ├── socket/handler/ # WebSocket 핸들러 (채팅·알림)
│ │ │ └── util/
│ │ │ ├── FileDTO.java
│ │ │ ├── FileUtil.java # UUID 기반 파일 업로드 유틸
│ │ │ └── pagination/
│ │ │ └── PagingV2.java # Oracle ROWNUM 기반 페이징 유틸
│ │ ├── exchange/ # 교환 처리
│ │ ├── inquiry/ # 문의 게시판
│ │ ├── member/ # 회원 관리
│ │ ├── mypage/ # 마이페이지
│ │ ├── notice/ # 공지사항
│ │ └── point/ # 포인트 시스템
│ ├── resources/mybatis/mapper/ # MyBatis XML 매퍼 (기능별)
│ └── webapp/
│ ├── WEB-INF/templates/ # Thymeleaf 템플릿 (기능별 폴더)
│ └── resources/
│ ├── css/ # 기능별 CSS
│ ├── js/ # 기능별 JS
│ └── vendor/ # Bootstrap, jQuery, FontAwesome
└── pom.xml
- 회원가입 시 Javax.mail API 이메일 인증으로 본인 확인
- Spring Security 기반 권한(B: 일반 / C: 관리자)·인증·Session 통합 관리
- remember-me 기능으로 자동 로그인, 동시 로그인 차단 처리
- 카카오 로그인 API 소셜 간편 로그인 구현
- 아이디(이메일) 찾기 / 비밀번호 재발급
- 회원 정보 수정
- 포인트 결제·환급 내역, 채팅 목록, 문의 내역, 알림 내역 통합 조회
- 카드 형식으로 물건 등록 후 원하는 카드와 교환 신청·수락·거절·취소·완료 처리
- 진행 중 / 신청 중 / 요청받은 카드 상태별 구분 조회
- 찜 기능으로 원하는 카드 저장 및 관리
- 무료 나눔 카드 등록 및 신청 기능
- 팝업창을 통한 카드 세부사항 출력 및 신청 처리
- Spring WebSocket + SockJS 를 이용한 실시간 1:1 채팅
- 교환 요청·수락·거절 등 상태 변경 시 실시간 알림 발송
- 읽지 않은 알림 표시 / 7일 이내 알림 필터링 / 페이징 처리
- 아임포트 API 를 통한 포인트 충전
- 금융결제원 API 계좌 실명 조회 후 포인트 환급 신청
- 포인트 사용·충전·환급 거래 내역 조회
- 정보공유 게시판 / 공지사항 / 문의 게시판 / 자주 묻는 질문
- 다중 이미지 업로드 및 AJAX 기반 비동기 댓글 처리
- URL 레벨 접근 제어 및 뷰 레벨 버튼 조건부 렌더링으로 이중 권한 처리
- 커서 기반 페이지네이션 및 최신 날짜순 정렬
- 조회수 기준 인기 카드 Top 5 / 평점 기준 인기 유저 Top 5 노출
- 전체 카드 실시간 모니터링 (n초 간격 자동 갱신)
- 회원 목록 조회·수정 및 상세 정보 확인
- 포인트 환급 신청 내역 검토 및 처리
기여도: 20% (팀 구성원 6명)
담당 역할: 기능 명세서 / UI 설계 / DB 설계 / 공통 모듈 설계 및 구현 / 커뮤니티 게시판 전담 구현 / 카드 교환 기능 공동 개발 참여
Controller → Service(Interface/Impl) → Repository(@Mapper) 계층으로 분리하였고,
SQL은 MyBatis XML 매퍼와 어노테이션 방식을 혼용하여 작성하였습니다.
작성
- gif·jpeg·png·jpg 형식의 이미지 다중 첨부를 지원하며, 업로드 전 클라이언트 측에서 파일 형식 유효성 검사 및 미리보기를 제공
- 서버에서는 원본 파일명을 UUID로 변환하여 저장하고, 원본명은 DB에 별도 보관하여 사용자에게는 원래 이름으로 노출
- 작성 완료 후 해당 게시글 상세 페이지로 즉시 리다이렉트
조회 (목록)
- Oracle
ROWNUM기반 커서 페이지네이션 적용, 최신 날짜순 정렬 - 총 게시글 수와 현재 페이지를 기반으로 이전·다음 블록 계산 및 페이지 번호 렌더링
조회 (상세)
- 게시글 정보와 첨부파일 목록을 하나의
Map으로 묶어 뷰에 전달 - 파일은 저장 경로와 변환된 파일명을 조합한 URL로 렌더링
수정
- 수정 폼 진입 시 기존 첨부 이미지 미리보기 제공
- 이미지 클릭으로 개별 삭제 가능하며, 삭제 대상 파일 ID를 hidden input으로 서버에 전달
삭제
- DB에서 레코드를 물리적으로 제거하지 않고 삭제 플래그(
is_del)를 갱신하는 소프트 딜리트 방식으로 처리하여 데이터 이력 보존
Spring Security로 URL 레벨 접근 제어를 적용하고, 뷰에서는 Thymeleaf Security Dialect로 인증된 사용자 정보를 조회하여 작성자 본인에게만 수정·삭제 버튼을 조건부 렌더링하는 이중 권한 처리 구조로 구성하였습니다.
// SecurityConfig.java - URL 레벨: 인증된 회원만 접근 허용
.mvcMatchers("/board/**").hasAnyAuthority("B", "C")<!-- board-detail.html - 뷰 레벨: 작성자 본인에게만 버튼 노출 -->
<span th:if="${#authentication.principal.memberNick} == ${datas.board.userId}">
<a th:href="@{/board/board-modify(bdIdx=${datas.board.bdIdx})}">수정</a>
<button>삭제</button>
</span>- 등록·삭제를 AJAX 비동기로 처리하여 페이지 전체 재로딩 없이 동작
- 댓글도 소프트 딜리트 방식으로 삭제 처리하여 이력 보존
상황
다수의 사용자가 동일한 파일명(예: image.png)으로 이미지를 업로드할 경우,
서버 저장 경로에서 파일이 서로 덮어써져 기존 게시글의 이미지가 손상될 위험이 있었습니다.
해결 방법
FileUtil.createFileDTO()에서 원본 파일명을 그대로 사용하지 않고,
UUID.randomUUID()로 생성한 고유값으로 파일명을 변환한 뒤 저장하도록 처리하였습니다.
원본 파일명은 originFileName으로 DB에 별도 보관하여 사용자에게는 원래 이름으로 노출합니다.
// FileUtil.java
String originFileName = mf.getOriginalFilename();
String renameFileName = UUID.randomUUID().toString();
if (originFileName.contains(".")) {
renameFileName += originFileName.substring(originFileName.lastIndexOf("."));
}
fileDTO.setOriginFileName(originFileName); // 원본명 보존
fileDTO.setRenameFileName(renameFileName); // UUID 변환명으로 저장또한 저장 경로를 연/월/일 단위 하위 폴더로 자동 분류하여,
파일 수가 많아져도 디렉토리가 과부하되지 않도록 구성하였습니다.
상황
파일 저장 중 IOException이 발생하더라도 호출부에서 별도 처리 없이 그냥 넘어가면
게시글은 DB에 저장되고 이미지만 누락되는 불일치 상태가 발생할 수 있었습니다.
해결 방법
FileUtil.fileUpload()에서 파일 저장 실패 시 HandlableException을 던지도록 처리하였습니다.
HandlableException은 @ControllerAdvice로 선언된 ExceptionAdvice에서 전역으로 잡아
사용자에게 에러 메시지와 리다이렉트 URL을 일관되게 응답합니다.
덕분에 업로드 실패 시 게시글 저장도 롤백되어 데이터 불일치를 방지할 수 있었습니다.
// FileUtil.java
try {
mf.transferTo(new File(getSavePath() + file.getRenameFileName()));
} catch (IllegalStateException | IOException e) {
throw new HandlableException(ErrorCode.FAILED_FILE_UPLOAD_ERROR, e);
}
// ExceptionAdvice.java (@ControllerAdvice)
@ExceptionHandler(HandlableException.class)
public String handlableExceptionProcess(HandlableException e, Model model) {
model.addAttribute("msg", e.error.MESSAGE);
model.addAttribute("url", e.error.URL);
return "common/result";
}상황
여러 사용자가 동시에 댓글을 등록할 경우 댓글 순서(cm_order)가 꼬일 수 있었습니다.
Oracle Sequence만으로는 게시글 단위의 댓글 순서를 보장하기 어려웠습니다.
해결 방법
댓글 등록 시 해당 게시글(bdIdx)의 현재 마지막 cm_order를 먼저 조회한 뒤,
+1을 더한 값을 새 댓글의 순서로 지정하도록 처리하였습니다.
NVL(MAX(cm_order), 0)을 사용하여 댓글이 없는 경우에도 안전하게 초기값 0을 반환합니다.
// BoardServiceImpl.java
public void boardReplyInsert(Reply reply) {
int lastOrder = boardRepository.selectLastOrderOfBoard(reply.getBdIdx());
reply.setCmOrder(lastOrder + 1);
boardRepository.insertReplyDepth1(reply);
}-- BoardRepository.java
@Select("select nvl(max(cm_order), 0) from reply WHERE bd_idx=#{bdIdx}")
int selectLastOrderOfBoard(int bdIdx);팀원들과 서로서로 도와가며 불가능해 보였던 프로젝트를 마무리 지었다는 것에 대한 자부심과,
시작할 때의 두려움이 있더라도 결국 하면 된다라는 교훈을 얻었습니다.
세미 프로젝트에 비해 규모가 커지면서 고려해야 할 부분이 많았고,
다양한 오류를 직접 마주하고 해결하면서 개발자로서 한 단계 성장할 수 있었습니다.
