대학생들이 수업, 프로젝트, 동아리 등에서 자신에게 맞는 팀원을 찾고, 동료평가를 통해 신뢰할 수 있는 협업 파트너를 선택할 수 있도록 돕는 플랫폼
🌐 서비스 바로가기 | 📖 API 문서 | 🎨 Figma
ERD 및 API 명세서는 두 사람이 함께 진행하였습니다.
| 이름 | 주요 담당 기능 |
|---|---|
| 🔥이상헌 | - OAuth 기반 로그인 - 사용자 프로필 관리 관련 기능 - AWS S3 파일 업로드 - 동료평가 시스템 |
| 🥷조귀호 | - mate check!(매칭 요청) -팀원 모집 관련 기능 |
| 항목 | 내용 |
|---|---|
| 🚀 배포 주소 | https://matecheck.vercel.app |
| 🔗 API Base URL | https://matecheck.co.kr |
| ⏱️ 개발 기간 | 3주 |
---
|
|
GitHub Repository
↓ (push)
EC2 Server
↓ (pull & build)
Spring Boot Application (Port 8080)
↓
Nginx (Port 80/443)
↓ (reverse proxy)
Client (matecheck.vercel.app)
배포 프로세스
- 로컬에서 GitHub로 push
- EC2 서버에서 수동으로 pull 후 빌드
- Nginx를 통한 리버스 프록시 및 HTTPS 적용
- S3를 통한 정적 파일(이미지) 관리
📦 src/main/java/pard/server/com/longkathon/
├── 📂 config/ # 설정 파일
│ ├── CorsConfig.java
│ ├── SecurityConfig.java
│ ├── SwaggerConfig.java
│ └── WebMvcConfig.java
├── 📂 googleLogin/ # OAuth 인증
│ ├── AuthController.java
│ ├── GoogleTokenParser.java
│ └── GoogleUserInfo.java
├── 📂 MyPage/ # 사용자 프로필 도메인
│ ├── user/
│ ├── userFile/
│ ├── introduction/
│ ├── activity/
│ ├── skillStackList/
│ ├── peerReview/
│ ├── peerGoodKeyword/
│ └── peerBadKeyword/
├── 📂 posting/ # 모집하기 도메인
│ ├── recruiting/
│ └── myKeyword/
├── 📂 poking/ # mate check! 도메인
├── 📂 alarm/ # 알림 도메인
└── 📂 s3/ # AWS S3 연동
- Java 17 이상
- MySQL 8.0 이상
- Gradle
src/main/resources/application.yml 파일을 생성하고 아래 내용을 입력합니다:
spring:
datasource:
url: jdbc:mysql://localhost:3306/{DB명}?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
username: {DB 사용자명}
password: {DB 비밀번호}
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update # 개발: update, 운영: validate 권장
show-sql: true
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.MySQL8Dialect
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
cloud:
aws:
credentials:
access-key: {AWS Access Key}
secret-key: {AWS Secret Key}
region:
static: ap-northeast-2
s3:
bucket: {S3 버킷명}
stack:
auto: false
logging:
level:
pard.server.com.longkathon: DEBUGCREATE DATABASE matecheck CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;# Gradle로 빌드
./gradlew build
# 애플리케이션 실행
./gradlew bootRun
# 또는 JAR 파일 실행
java -jar build/libs/Longkathon-0.0.1-SNAPSHOT.jar- 🌐 서버: http://localhost:8080
- 📚 Swagger UI: http://localhost:8080/swagger-ui/index.html
1. 서비스 소개 페이지 API
Endpoint: GET /user/firstPage
Response:
{
"profileFeedList": [
{
"userId": "long",
"name": "string",
"firstMajor": "string",
"secondMajor": "string",
"studentId": "string",
"introduction": "string",
"skillList": ["string"],
"peerGoodKeywords": ["string"],
"imageUrl": "string"
}
],
"recruitingFeedList": [
{
"recruitingId": "long",
"name": "string",
"projectType": "string",
"projectSpecific": "string",
"classes": "string",
"topic": "string",
"totalPeople": "integer",
"recruitPeople": "integer",
"title": "string",
"myKeyword": ["string"]
}
]
}profileFeedList: 최근 프로필 피드 3개recruitingFeedList: 최근 팀원 구하기 게시글 3개
2. 구글 로그인 API
Endpoint: POST /auth/google/exists
프론트에서 구글로그인을 통한 idToken을 넘겨준다.
Response:
{
"exists": "boolean",
"email": "string",
"socialId": "string",
"myId": "Long"
}exists: DB에 해당 유저가 존재하면 true → 회원가입페이지 스킵하고 바로 메인페이지로myId: DB에 해당 유저가 존재한다면, 유저의 id값. 앞으로 프론트는 해당 유저에 대한 페이지를 요청할때 myId로 요청한다.
3. 회원가입 (User) API
Endpoint: POST /user/create
Request Body: profileImage와 data (JSON)을 둘다 보내야함
const formData = new FormData();
formData.append("profileImage", file);
formData.append("data", JSON.stringify(payload));
await axios.post("http://localhost:8080/user/create", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});{
"name": "string",
"studentId": "string",
"grade": "string",
"semester": "string",
"department": "string",
"firstMajor": "string",
"secondMajor": "string",
"phoneNumber": "string (optional)",
"gpa": "string (optional)",
"email": "string",
"socialId": "string"
}Response:
{
"myId": "long",
"name": "string"
}Endpoint: GET /user/findAll
Response: List of
{
"userId": "long",
"name": "string",
"firstMajor": "string",
"secondMajor": "string",
"studentId": "string",
"introduction": "string",
"skillList": ["string"],
"peerGoodKeywords": ["string"],
"imageUrl": "string"
}Endpoint: GET /user/filter
Query Parameters:
departments: string (comma-separated, 예: "컴퓨터공학과,전자공학과")name: string (예: "홍길동")
Example:
GET /user/filter?departments=컴퓨터공학과,전자공학과&name=홍길동
Response: List of
{
"userId": "long",
"name": "string",
"firstMajor": "string",
"secondMajor": "string",
"studentId": "string",
"introduction": "string",
"skillList": ["string"],
"peerGoodKeywords": ["string"],
"imageUrl": "string",
"goodKeywordCount": "int"
}Endpoint: GET /user/equal/{myId}/{userId}
두 사용자 ID가 동일한지 비교합니다.
Response: boolean
true // 동일한 경우
false // 다른 경우흐름:
GET /user/equal/{myId}/{userId}먼저 요청- false일때는
GET /user/mateProfile/{userId}요청 - true일때는
GET /user/myProfile/{myId}요청
Endpoint: GET /user/mateProfile/{userId}
Response:
{
"name": "string",
"email": "string",
"department": "string",
"firstMajor": "string",
"secondMajor": "string",
"gpa": "string",
"grade": "string",
"studentId": "string",
"semester": "integer",
"imageUrl": "string",
"introduction": "string",
"skillList": ["string"],
"activity": [
{
"year": "integer",
"title": "string",
"link": "string"
}
],
"peerGoodKeyword": {
"keyword1": "integer",
"keyword2": "integer",
"keyword3": "integer"
},
"goodKeywordCount": "integer",
"peerBadKeyword": {
"keyword1": "integer",
"keyword2": "integer",
"keyword3": "integer"
},
"badKeywordCount": "integer",
"peerReviewRecent": [
{
"startDate": "string",
"meetSpecific": "string",
"goodKeywordList": ["string"],
"badKeywordList": ["string"]
}
]
}Endpoint: GET /user/myProfile/{myId}
Response:
{
"name": "string",
"email": "string",
"department": "string",
"firstMajor": "string",
"secondMajor": "string",
"gpa": "string",
"grade": "string",
"studentId": "string",
"semester": "integer",
"imageUrl": "string",
"introduction": "string",
"skillList": ["string"],
"activity": [
{
"year": "integer",
"title": "string",
"link": "string"
}
]
}Endpoint: POST /user/updateImage/{myId}
Request Body:
POST /user/updateImage/123
Content-Type: multipart/form-data
profileImage: [image file]
Response: 200 OK
Endpoint: DELETE /user/myProfile/{myId}
Response: 200 OK
Endpoint: PATCH /user/update/{myId}
Request Body:
{
"name": "string",
"email": "string",
"department": "string",
"firstMajor": "string",
"secondMajor": "string",
"gpa": "string",
"studentId": "string",
"grade": "string",
"semester": "string",
"imageUrl": "string",
"introduction": "string",
"skillList": ["string", "string"],
"activity": [
{
"year": "string",
"title": "string",
"link": "string"
}
]
}Response: 200 OK
Endpoint: GET /user/myPeerReview/{myId}
Response:
{
"peerGoodKeyword": {
"keyword1": "integer",
"keyword2": "integer",
"keyword3": "integer"
},
"goodKeywordCount": "integer",
"peerBadKeyword": {
"keyword1": "integer",
"keyword2": "integer",
"keyword3": "integer"
},
"badKeywordCount": "integer",
"peerReviewRecent": [
{
"startDate": "string",
"meetSpecific": "string",
"goodKeywordList": ["string"],
"badKeywordList": ["string"]
}
]
}4. 모집 (Recruiting) API
Endpoint: GET /recruiting/findAll
Response: List of
{
"recruitingId": "long",
"name": "string",
"projectType": "string",
"projectSpecific": "string",
"classes": "string",
"topic": "string",
"totalPeople": "integer",
"recruitPeople": "integer",
"title": "string",
"myKeyword": ["string"],
"date": "string"
}Endpoint: GET /recruiting/filter
Query Parameters:
type: string (예: "수업", "졸작", "동아리", "학회", "대회")departments: string (comma-separated, 예: "컴퓨터공학과,전자공학과")name: string (예: "홍길동")
Response: List of
{
"recruitingId": "long",
"name": "string",
"projectType": "string",
"projectSpecific": "string",
"classes": "integer",
"topic": "string",
"totalPeople": "integer",
"recruitPeople": "integer",
"title": "string",
"myKeyword": ["string"],
"date": "string"
}Endpoint: GET /recruiting/{myId}
Response: List of
{
"recruiting": "long",
"name": "string",
"projectType": "string",
"projectSpecific": "string",
"classes": "string",
"topic": "string",
"totalPeople": "integer",
"recruitPeople": "integer",
"title": "string",
"myKeyword": ["string"],
"date": "string"
}Endpoint: GET /recruiting/detail/{recruitingId}/{myId}
Response:
{
"name": "string",
"projectType": "string",
"projectSpecific": "string",
"classes": "string",
"topic": "string",
"totalPeople": "integer",
"recruitPeople": "integer",
"title": "string",
"myKeyword": ["string"],
"date": "string",
"context": "string",
"studentId": "string",
"firstMajor": "string",
"secondMajor": "string",
"imageUrl": "string",
"postingList": [
{
"recruitingId": "long",
"name": "string",
"projectType": "string",
"totalPeople": "integer",
"recruitPeople": "integer",
"title": "string",
"date": "string"
}
],
"canEdit": "Boolean"
}Notes: 작성자와 로그인 계정이 동일하면 수정 가능한 UI, 다르면 수정 불가한 UI 제공
Endpoint: PATCH /recruiting/{recruitingId}/{myId}
Request Body:
{
"projectType": "string",
"projectSpecific": "string",
"classes": "string",
"topic": "string",
"totalPeople": 0,
"recruitPeople": 0,
"title": "string",
"context": "string",
"keyword": ["string", "string", "..."]
}Response: 200 OK
Endpoint: DELETE /recruiting/{recruitingId}/{myId}
Response: 200 OK
Endpoint: POST /recruiting/createPost/{userId}
Request Body:
{
"projectType": "string",
"projectSpecific": "string",
"classes": "string",
"topic": "string",
"totalPeople": "integer",
"recruitPeople": "integer",
"title": "string",
"context": "string",
"myKeyword": ["string"]
}Response: 200 OK
5. 동료평가 (Peer Review) API
Endpoint: POST /peerReview/{myId}/{userId}
Path Parameters:
myId: 평가 작성자 IDuserId: 평가 대상자 ID
Request Body:
{
"startDate": "string",
"meetSpecific": "string",
"goodKeywordList": ["string"],
"badKeywordList": ["string"]
}Response: 200 OK (평가 생성 완료)
6. mate check! (Poking) API
Endpoint: POST /poking/{recruitingId}/{myId}
Response: 200 OK (생성 완료)
Endpoint: POST /poking/user/{userId}/{myId}
Path Parameters:
userId: mate check!를 받는 사람(메이트)myId: mate check!를 보내는 사람(로그인 유저)
Response: 200 OK (생성 완료)
Endpoint: GET /poking/canInRecruiting/{recruitingId}/{myId}
Response:
{
"canPoke": "boolean",
"reason": "string"
}Endpoint: GET /poking/canInProfile/{userId}/{myId}
Response:
{
"canPoke": "boolean",
"reason": "string"
}Endpoint: GET /poking/received/{myId}
Response: List of
[
{
"pokingId": "long",
"recruitingId": "long",
"senderId": "long",
"senderName": "string",
"projectSpecific": "string",
"date": "string",
"imageUrl": "string"
}
]mate check!를 삭제하며, 수락(true) / 거절(false) 여부에 따라 서버 내부에서 알림이 생성됩니다.
Endpoint: DELETE /poking/{pokingId}
Request Body:
{
"ok": "boolean"
}Endpoint: GET /alarm/{userId}
Response: List of
[
{
"alarmId": 1,
"senderName": "홍길동",
"ok": true
},
{
"alarmId": 2,
"senderName": "김철수",
"ok": false
}
]Endpoint: DELETE /alarm/{alarmId}
Response: 200 OK
mate check! 수락(
ok=true) 처리 시 채팅 생성 로직이 함께 수행될 수 있습니다.
Endpoint: DELETE /poking/{pokingId}
Response: 200 OK
-
Google 로그인 방식 정리
- 문제: “일반 로그인(JWT/세션)”과 달리
idToken기반 흐름이라 팀 내에서 인증/인가 범위가 헷갈림 - 대응:
POST /auth/google/exists에서idToken → (email/socialId) 추출 → exists/myId 반환흐름으로 문서화
- 문제: “일반 로그인(JWT/세션)”과 달리
-
EC2 수동 배포로 인한 설정 불일치
- 문제: 로컬과 EC2 환경변수/설정 파일 차이로 실행 오류가 발생하기 쉬움
- 대응:
application.yml분리 + 환경변수 목록을 정리하고, 배포 시 체크리스트(필수 env, DB 연결, S3 권한)를 만들어 공유
-
이미지 업로드(멀티파트) 디버깅
- 문제:
multipart/form-data에서profileImage+data(JSON string)를 함께 전송할 때 키 이름/Content-Type 실수로 400/415 발생 - 대응: 프론트 요청 예시(FormData)와 서버 요구 파라미터 이름을 명세에 고정, Postman으로 먼저 검증 후 프론트 적용
- 문제:
-
ERD/도메인 설계 변경 비용
- 문제: 화면/API 변경이 생길 때 ERD가 같이 흔들리며 수정 비용이 커짐
- 대응: 키워드/리뷰처럼 변화가 잦은 영역은 “정규화 vs 집계 테이블” 기준을 세우고, 누적 집계(GOOD/BAD) 테이블로 조회 성능을 확보
-
서버 시간대(UTC)로 인해 생성 시간이 한국 시간과 다르게 저장됨
- 문제: AWS 서버 리전/기본 타임존 설정 영향으로
LocalDateTime.now()기준 시간이 한국 시간(KST)과 어긋나, 모집글/찌르기 생성 시간이 프론트에서 기대한 시간과 다르게 보임 - 원인: 서버 환경(예: UTC 또는 다른 타임존) 기준으로 애플리케이션 시간이 생성됨
- 대응:
Recruiting,Poking엔티티에@PrePersist를 추가해 저장 직전에 KST(Asia/Seoul) 기준으로date를 세팅
Recruiting
import java.time.ZoneId; @PrePersist public void prePersist() { if (this.date == null) { this.date = LocalDateTime.now(ZoneId.of("Asia/Seoul")) .truncatedTo(java.time.temporal.ChronoUnit.MINUTES); } }
Poking
import java.time.ZoneId; @PrePersist public void prePersist() { if (this.date == null) { this.date = LocalDateTime.now(ZoneId.of("Asia/Seoul")); } }
- 문제: AWS 서버 리전/기본 타임존 설정 영향으로
-
3주 내 “기능 완성 + 배포”까지 도달
- OAuth 기반 인증 흐름을 구현하고, Nginx/HTTPS를 포함한 실서비스 형태로 끝까지 연결
-
조회 성능을 고려한 설계 시도
- 동료평가 키워드 Top3/Count 요구사항을 누적 집계 테이블로 분리해 조회를 단순화
-
API 명세 중심 협업
- 화면 흐름(동일 유저 여부 판별, canEdit 등)을 명세에 반영해 프론트와 합의점을 만들고 개발 진행
-
인증 고도화
idToken은 “최초 로그인 검증”에만 사용- 서버가
access/refresh JWT를 발급하고, 권한/만료/재발급 흐름을 표준화
-
Docker 기반 배포 전환
- Spring + MySQL(+Nginx) 컨테이너화로 실행 환경을 고정
docker compose로 로컬/서버 환경을 동일하게 맞추기
-
CI/CD 도입
- GitHub Actions로 빌드/테스트 후 EC2 배포 자동화
- 배포 실패 시 롤백 또는 이전 버전 유지 전략 수립
-
운영 기본기 추가
- 로그(구조화 로그), 모니터링(헬스체크/메트릭) 적용
- 프로세스 매니저(systemd) 또는 컨테이너 재시작 정책으로 안정성 확보
-
DB/도메인 리팩토링
- 변화가 잦은 도메인(키워드/리뷰/프로필 확장)에 대해 스키마 정책 정리
- 인덱스/쿼리 튜닝 및 조회 API의 페이지네이션 도입



















