๋ํ์๋ค์ด ์์ , ํ๋ก์ ํธ, ๋์๋ฆฌ ๋ฑ์์ ์์ ์๊ฒ ๋ง๋ ํ์์ ์ฐพ๊ณ , ๋๋ฃํ๊ฐ๋ฅผ ํตํด ์ ๋ขฐํ ์ ์๋ ํ์ ํํธ๋๋ฅผ ์ ํํ ์ ์๋๋ก ๋๋ ํ๋ซํผ
๐ ์๋น์ค ๋ฐ๋ก๊ฐ๊ธฐ | ๐ 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
- ๋ก์ปฌ: http://localhost:8080/swagger-ui/index.html
- ์ด์: https://matecheck.co.kr/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์ ํ์ด์ง๋ค์ด์ ๋์



















