Skip to content

Commit 41f12b8

Browse files
committed
merge: recent dev on 0924.1441
2 parents cb03b16 + 2cbcc82 commit 41f12b8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1279
-79
lines changed

.github/workflows/deploy.yml

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
name: deploy
2+
3+
# =========================
4+
# 전역 환경변수
5+
# =========================
6+
env:
7+
IMAGE_REPOSITORY: backend # GHCR 이미지 리포지토리명(소유자 포함 X)
8+
CONTAINER_1_NAME: backend
9+
EC2_INSTANCE_TAG_NAME: team2-ec2-1 # 배포 대상 EC2 Name 태그
10+
DOCKER_NETWORK: common # 도커 네트워크
11+
BACKEND_DIR: . # Dockerfile 위치
12+
13+
14+
on:
15+
push:
16+
paths:
17+
- ".github/workflows/**"
18+
- "src/**"
19+
- "build.gradle.kts"
20+
- "settings.gradle.kts"
21+
- "Dockerfile"
22+
- "terraform/main.tf"
23+
branches:
24+
- main
25+
26+
# 권한 최소화/명시화
27+
permissions:
28+
contents: write # 태그/릴리즈
29+
packages: write # GHCR 푸시
30+
31+
# 기본 셸
32+
defaults:
33+
run:
34+
shell: bash
35+
36+
jobs:
37+
# ---------------------------------------------------------
38+
# 1) 태그/릴리즈 생성
39+
# ---------------------------------------------------------
40+
makeTagAndRelease:
41+
runs-on: ubuntu-latest
42+
outputs:
43+
tag_name: ${{ steps.create_tag.outputs.new_tag }} # 이후 잡에서 사용할 태그명
44+
steps:
45+
- uses: actions/checkout@v4
46+
47+
# 버전 태그 자동 생성 (vX.Y.Z)
48+
- name: Create Tag
49+
id: create_tag
50+
uses: mathieudutour/[email protected]
51+
with:
52+
github_token: ${{ secrets.GIT_TOKEN }}
53+
54+
# 릴리즈 생성
55+
- name: Create Release
56+
id: create_release
57+
uses: actions/create-release@v1
58+
env:
59+
GITHUB_TOKEN: ${{ secrets.GIT_TOKEN }}
60+
with:
61+
tag_name: ${{ steps.create_tag.outputs.new_tag }}
62+
release_name: Release ${{ steps.create_tag.outputs.new_tag }}
63+
body: ${{ steps.create_tag.outputs.changelog }}
64+
draft: false
65+
prerelease: false
66+
67+
# ---------------------------------------------------------
68+
# 2) 도커 이미지 빌드/푸시 (캐시 최대 활용)
69+
# ---------------------------------------------------------
70+
buildImageAndPush:
71+
name: 도커 이미지 빌드와 푸시
72+
needs: makeTagAndRelease
73+
runs-on: ubuntu-latest
74+
steps:
75+
- uses: actions/checkout@v4
76+
77+
# 빌드 컨텍스트에 .env 생성 (비어있어도 실패하지 않게)
78+
- name: .env 파일 생성
79+
env:
80+
DOT_ENV: ${{ secrets.DOT_ENV }}
81+
run: |
82+
# .env가 없으면 빌드 캐시가 매번 깨질 수 있으므로 항상 생성
83+
mkdir -p "${{ env.BACKEND_DIR }}"
84+
printf "%s" "${DOT_ENV}" > "${{ env.BACKEND_DIR }}/.env"
85+
86+
- name: Docker Buildx 설치
87+
uses: docker/setup-buildx-action@v3
88+
89+
# GHCR 로그인
90+
- name: 레지스트리 로그인
91+
uses: docker/login-action@v3
92+
with:
93+
registry: ghcr.io
94+
username: ${{ github.actor }}
95+
password: ${{ secrets.GIT_TOKEN }}
96+
97+
# 저장소 소유자명을 소문자로 (GHCR 경로 표준화)
98+
- name: set lower case owner name
99+
run: |
100+
echo "OWNER_LC=${OWNER,,}" >> "${GITHUB_ENV}"
101+
env:
102+
OWNER: "${{ github.repository_owner }}"
103+
104+
# 캐시를 최대한 활용하여 빌드 → 버전태그 및 latest 동시 푸시
105+
- name: 빌드 앤 푸시
106+
uses: docker/build-push-action@v6
107+
with:
108+
context: ${{ env.BACKEND_DIR }}
109+
push: true
110+
cache-from: type=gha
111+
cache-to: type=gha,mode=max
112+
tags: |
113+
ghcr.io/${{ env.OWNER_LC }}/${{ env.IMAGE_REPOSITORY }}:${{ needs.makeTagAndRelease.outputs.tag_name }}
114+
ghcr.io/${{ env.OWNER_LC }}/${{ env.IMAGE_REPOSITORY }}:latest
115+
116+
# ---------------------------------------------------------
117+
# 3) 배포
118+
# ---------------------------------------------------------
119+
deploy:
120+
name: 배포
121+
runs-on: ubuntu-latest
122+
needs: [makeTagAndRelease, buildImageAndPush]
123+
steps:
124+
# AWS 자격 구성
125+
- uses: aws-actions/configure-aws-credentials@v4
126+
with:
127+
aws-region: ${{ secrets.AWS_REGION }}
128+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
129+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
130+
131+
# Name 태그로 EC2 인스턴스 조회 (없으면 실패)
132+
- name: 인스턴스 ID 가져오기
133+
id: get_instance_id
134+
run: |
135+
INSTANCE_ID=$(aws ec2 describe-instances \
136+
--filters "Name=tag:Name,Values=${{ env.EC2_INSTANCE_TAG_NAME }}" "Name=instance-state-name,Values=running" \
137+
--query "Reservations[].Instances[].InstanceId" --output text)
138+
[[ -n "${INSTANCE_ID}" && "${INSTANCE_ID}" != "None" ]] || { echo "No running instance found"; exit 1; }
139+
echo "INSTANCE_ID=${INSTANCE_ID}" >> "${GITHUB_ENV}"
140+
141+
# 원격(SSM)으로 배포 수행
142+
- name: AWS SSM Send-Command
143+
uses: peterkimzz/aws-ssm-send-command@master
144+
with:
145+
aws-region: ${{ secrets.AWS_REGION }}
146+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
147+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
148+
instance-ids: ${{ env.INSTANCE_ID }}
149+
working-directory: /
150+
comment: Deploy
151+
command: |
152+
set -Eeuo pipefail
153+
154+
LOG="/tmp/ssm-$(date +%Y%m%d_%H%M%S).log"
155+
exec > >(awk '{ fflush(); print strftime("[%Y-%m-%d %H:%M:%S]"), $0 }' | tee -a "$LOG")
156+
exec 2> >(awk '{ fflush(); print strftime("[%Y-%m-%d %H:%M:%S]"), $0 }' | tee -a "$LOG" >&2)
157+
158+
source /etc/environment || true
159+
160+
OWNER_LC="${{ github.repository_owner }}"
161+
OWNER_LC="${OWNER_LC,,}"
162+
IMAGE_TAG='${{ needs.makeTagAndRelease.outputs.tag_name }}'
163+
IMAGE_REPOSITORY='${{ env.IMAGE_REPOSITORY }}'
164+
IMAGE="ghcr.io/${OWNER_LC}/${IMAGE_REPOSITORY}:${IMAGE_TAG}"
165+
CONTAINER_1_NAME="${{ env.CONTAINER_1_NAME }}"
166+
NET="${{ env.DOCKER_NETWORK }}"
167+
168+
docker pull $IMAGE
169+
docker stop $CONTAINER_1_NAME || true
170+
docker rm $CONTAINER_1_NAME || true
171+
docker run -d --restart unless-stopped --name $CONTAINER_1_NAME --network $NET $IMAGE
172+
{
173+
docker images --format '{{.Repository}}:{{.Tag}}' \
174+
| grep -F "ghcr.io/${OWNER_LC}/${IMAGE_REPOSITORY}:" \
175+
| grep -v -F ":${IMAGE_TAG}" \
176+
| grep -v -F ":latest" \
177+
| xargs -r docker rmi
178+
} || true

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ db_dev.trace.db
4343
### Environment Variables ###
4444
.env
4545

46+
### Terraform ###
47+
.terraform
48+
.terraform.lock.hcl
49+
terraform.tfstate
50+
terraform.tfstate.backup
51+
.terraform.tfstate.lock.info
52+
terraform/secrets.tf
53+
54+
4655
### Claude AI ###
4756
CLAUDE.md
4857
.claude/

Dockerfile

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
# 첫 번째 스테이지: 빌드 스테이지
3+
FROM gradle:jdk-21-and-23-graal-jammy AS builder
4+
5+
# 작업 디렉토리 설정
6+
WORKDIR /app
7+
8+
# 소스 코드와 Gradle 래퍼 복사
9+
COPY build.gradle.kts .
10+
COPY settings.gradle.kts .
11+
12+
# 종속성 설치
13+
RUN gradle dependencies --no-daemon
14+
15+
# 소스 코드 복사
16+
COPY .env .
17+
COPY src src
18+
19+
# 애플리케이션 빌드
20+
RUN gradle build --no-daemon
21+
22+
# 두 번째 스테이지: 실행 스테이지
23+
FROM container-registry.oracle.com/graalvm/jdk:21
24+
25+
# 작업 디렉토리 설정
26+
WORKDIR /app
27+
28+
# 첫 번째 스테이지에서 빌드된 JAR 파일 복사
29+
COPY --from=builder /app/build/libs/*.jar app.jar
30+
COPY --from=builder /app/.env .env
31+
32+
# 실행할 JAR 파일 지정
33+
ENTRYPOINT ["java", "-Dspring.profiles.active=prod", "-jar", "app.jar"]

build.gradle.kts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,22 @@ dependencies {
3232
implementation("io.github.cdimascio:java-dotenv:5.2.2")
3333
implementation("org.springframework.boot:spring-boot-starter-security")
3434
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
35-
implementation("org.springframework.boot:spring-boot-starter-data-redis")
35+
3636
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0")
3737
implementation("io.jsonwebtoken:jjwt-api:0.12.3")
3838
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.3")
3939
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.3")
4040
compileOnly("org.projectlombok:lombok")
4141
developmentOnly("org.springframework.boot:spring-boot-devtools")
42+
43+
//prod
44+
implementation("org.springframework.boot:spring-boot-starter-actuator")
45+
implementation("org.springframework.boot:spring-boot-starter-data-redis")
46+
implementation("org.springframework.session:spring-session-data-redis")
47+
4248
runtimeOnly("com.h2database:h2")
49+
runtimeOnly("com.mysql:mysql-connector-j")
50+
4351
annotationProcessor("org.projectlombok:lombok")
4452

4553

docker-compose.yml

Lines changed: 0 additions & 16 deletions
This file was deleted.

src/main/java/com/back/BackApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import io.github.cdimascio.dotenv.Dotenv;
44
import org.springframework.boot.SpringApplication;
55
import org.springframework.boot.autoconfigure.SpringBootApplication;
6+
import org.springframework.cache.annotation.EnableCaching;
67
import org.springframework.scheduling.annotation.EnableScheduling;
78

89
@SpringBootApplication
910
@EnableScheduling
11+
@EnableCaching
1012
public class BackApplication {
1113

1214
public static void main(String[] args) {

src/main/java/com/back/domain/cocktail/controller/CocktailController.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public RsData<CocktailDetailResponseDto> getCocktailDetailById(@PathVariable lon
2929
return RsData.successOf(cocktailDetailResponseDto);
3030
}
3131

32-
// @param lastId 마지막으로 가져온 칵테일 ID (첫 요청 null 가능)
32+
// @param lastId 마지막으로 가져온 칵테일 ID (첫 요청 null 가능)
3333
// @param size 가져올 데이터 개수 (기본값 DEFAULT_SIZE)
3434
// @return RsData 형태의 칵테일 요약 정보 리스트
3535
@GetMapping
@@ -43,7 +43,6 @@ public RsData<List<CocktailSummaryResponseDto>> getCocktails(
4343
return RsData.successOf(cocktails);
4444
}
4545

46-
4746
// 칵테일 검색 및 필터링
4847
// POST 방식으로 JSON body를 통해 검색 조건 전달
4948

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.back.domain.cocktail.controller;
2+
3+
import com.back.domain.cocktail.repository.CocktailRepository;
4+
import com.back.global.rsData.RsData;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.http.HttpStatus;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.web.bind.annotation.GetMapping;
9+
import org.springframework.web.bind.annotation.PathVariable;
10+
import org.springframework.web.bind.annotation.RequestMapping;
11+
import org.springframework.web.bind.annotation.RestController;
12+
13+
import java.util.Map;
14+
15+
@RestController
16+
@RequestMapping("/api/cocktails")
17+
@RequiredArgsConstructor
18+
public class CocktailShareController {
19+
private final CocktailRepository cocktailRepository;
20+
21+
@GetMapping("/{id}/share")
22+
public ResponseEntity<RsData<Map<String, String>>> getShareLink(@PathVariable Long id) {
23+
return cocktailRepository.findById(id)
24+
.map(cocktail -> {
25+
Map<String, String> response = Map.of(
26+
// 공유 URL
27+
"url", "https://www.ssoul.or/cocktails/" + cocktail.getCocktailId(),
28+
// 공유 제목
29+
"title", cocktail.getCocktailName(),
30+
// 공유 이미지 (선택)
31+
"imageUrl", cocktail.getCocktailImgUrl()
32+
);
33+
return ResponseEntity.ok(RsData.successOf(response));
34+
})
35+
.orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND)
36+
.body(RsData.failOf("칵테일을 찾을 수 없습니다.")));
37+
}
38+
}

src/main/java/com/back/domain/myhistory/controller/MyHistoryController.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ public RsData<MyHistoryPostListDto> getMyPosts(
3333
MyHistoryPostListDto body = myHistoryService.getMyPosts(userId, lastCreatedAt, lastId, limit);
3434
return RsData.successOf(body);
3535
}
36+
37+
@GetMapping("/posts/{id}")
38+
public RsData<com.back.domain.myhistory.dto.MyHistoryPostGoResponseDto> goFromPost(
39+
@AuthenticationPrincipal(expression = "id") Long userId,
40+
@PathVariable("id") Long postId
41+
) {
42+
var body = myHistoryService.getPostLinkFromMyPost(userId, postId);
43+
return RsData.successOf(body);
44+
}
3645

3746
@GetMapping("/comments")
3847
public RsData<MyHistoryCommentListDto> getMyComments(
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.back.domain.myhistory.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@AllArgsConstructor
8+
public class MyHistoryPostGoResponseDto {
9+
private Long postId;
10+
private String postApiUrl;
11+
}
12+

0 commit comments

Comments
 (0)