Skip to content

Commit b45e0b8

Browse files
authored
Merge pull request #122 from Pinback-Team/dev
2차 스프린트 prod 업데이트
2 parents a69f6fb + 8b9d7af commit b45e0b8

63 files changed

Lines changed: 1470 additions & 58 deletions

File tree

Some content is hidden

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

.github/workflows/dev.yml

Lines changed: 72 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -113,83 +113,120 @@ jobs:
113113
- name: Checkout repository
114114
uses: actions/checkout@v4
115115

116-
# 2. SSH Agent 액션 추가
117-
- name: Set up SSH agent
118-
uses: webfactory/ssh-agent@v0.9.0
119-
with:
120-
ssh-private-key: |
121-
${{ github.ref == 'refs/heads/dev' && secrets.DEV_EC2_SSH_KEY || secrets.EC2_SSH_KEY }}
122-
123-
# 3. 환경에 따라 SSH 연결 정보 및 경로 설정
124-
- name: Set Deploy Environment
125-
id: set-env
116+
# 2. SSH 접속에 필요한 환경 변수 설정
117+
- name: Set Environment Variables
118+
id: set-env-vars
126119
shell: bash
127120
run: |
128121
if [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then
129122
echo "HOST=${{ secrets.DEV_EC2_SSH_HOST }}" >> $GITHUB_ENV
130123
echo "USERNAME=${{ secrets.DEV_EC2_SSH_USERNAME }}" >> $GITHUB_ENV
131-
{
132-
echo 'SSH_KEY<<EOF'
133-
echo '${{ secrets.DEV_EC2_SSH_KEY }}'
134-
echo 'EOF'
135-
} >> "$GITHUB_ENV"
136124
echo "DEPLOY_DIR=/home/ubuntu/pinback-dev" >> $GITHUB_ENV
125+
echo "ENV_SECRET_NAME=DEV_ENV_CONTENT" >> $GITHUB_ENV
137126
elif [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
138127
echo "HOST=${{ secrets.EC2_SSH_HOST }}" >> $GITHUB_ENV
139128
echo "USERNAME=${{ secrets.EC2_SSH_USERNAME }}" >> $GITHUB_ENV
140-
{
141-
echo 'SSH_KEY<<EOF'
142-
echo '${{ secrets.EC2_SSH_KEY }}'
143-
echo 'EOF'
144-
} >> "$GITHUB_ENV"
145129
echo "DEPLOY_DIR=/home/ubuntu/pinback" >> $GITHUB_ENV
130+
echo "ENV_SECRET_NAME=PROD_ENV_CONTENT" >> $GITHUB_ENV
146131
fi
147132
148-
# 비대화형 접속 위해 known_hosts 등록
133+
# 3. known_hosts 등록
149134
- name: Add host to known_hosts
150135
run: |
151136
mkdir -p ~/.ssh
152137
ssh-keyscan -p 22 ${{ env.HOST }} >> ~/.ssh/known_hosts
153138
154-
# 4. docker-compose.yml 파일을 EC2로 복사
139+
# 4. 원격 서버 초기화
140+
- name: Initialize remote server (Force Init)
141+
uses: appleboy/ssh-action@master
142+
with:
143+
host: ${{ env.HOST }}
144+
username: ${{ env.USERNAME }}
145+
key: ${{ github.ref == 'refs/heads/dev' && secrets.DEV_EC2_SSH_KEY || secrets.EC2_SSH_KEY }}
146+
debug: true
147+
script: |
148+
# 1. 배포 디렉토리 생성
149+
mkdir -p ${{ env.DEPLOY_DIR }}
150+
151+
# 2. 디렉토리 소유권 및 권한 강제 변경
152+
sudo chown -R ${{ env.USERNAME }}:${{ env.USERNAME }} ${{ env.DEPLOY_DIR }}
153+
154+
# 3. 기존 파일 강제 삭제
155+
sudo rm -f ${{ env.DEPLOY_DIR }}/docker-compose.yml || true
156+
sudo rm -f ${{ env.DEPLOY_DIR }}/pinback.env || true
157+
158+
# 5. docker-compose.yml 파일 복사
155159
- name: Copy docker-compose.yml to EC2
156160
uses: appleboy/scp-action@master
157161
with:
158162
host: ${{ env.HOST }}
159163
username: ${{ env.USERNAME }}
160-
key: ${{ env.SSH_KEY }}
164+
key: ${{ github.ref == 'refs/heads/dev' && secrets.DEV_EC2_SSH_KEY || secrets.EC2_SSH_KEY }}
161165
source: "docker-compose.yml"
162-
target: "${{ env.DEPLOY_DIR }}"
166+
target: ${{ env.DEPLOY_DIR }}
163167
debug: true
168+
overwrite: true
164169

165-
# 5. 배포 (docker composee(v2))
170+
# 6. pinback.env 파일을 Runner에 임시 생성 후 복사
171+
- name: Create & Copy pinback.env to EC2
172+
run: |
173+
# 환경 변수 이름으로 Secret 값을 가져와 파일 생성
174+
175+
if [[ "${{ env.ENV_SECRET_NAME }}" == "DEV_ENV_CONTENT" ]]; then
176+
echo "${{ secrets.DEV_ENV_CONTENT }}" > pinback.env
177+
elif [[ "${{ env.ENV_SECRET_NAME }}" == "PROD_ENV_CONTENT" ]]; then
178+
echo "${{ secrets.PROD_ENV_CONTENT }}" > pinback.env
179+
fi
180+
181+
# 6 - 1
182+
- name: Copy pinback.env file
183+
uses: appleboy/scp-action@master
184+
with:
185+
host: ${{ env.HOST }}
186+
username: ${{ env.USERNAME }}
187+
key: ${{ github.ref == 'refs/heads/dev' && secrets.DEV_EC2_SSH_KEY || secrets.EC2_SSH_KEY }}
188+
source: "pinback.env"
189+
target: ${{ env.DEPLOY_DIR }}
190+
debug: true
191+
overwrite: true
192+
193+
# 7. 배포 (docker composee(v2))
166194
- name: Deploy with Docker Compose to EC2
167195
uses: appleboy/ssh-action@master
168196
with:
169197
host: ${{ env.HOST }}
170198
username: ${{ env.USERNAME }}
171-
key: ${{ env.SSH_KEY }}
199+
key: ${{ github.ref == 'refs/heads/dev' && secrets.DEV_EC2_SSH_KEY || secrets.EC2_SSH_KEY }}
172200
debug: true
173201
script: |
202+
# DEPLOY_DIR 변수를 사용하기 위해 Bash 변수로 재선언
203+
DEPLOY_DIR=${{ env.DEPLOY_DIR }}
204+
174205
# 1) 배포 디렉터리로 이동
175-
mkdir -p "${{ env.DEPLOY_DIR }}" # 디렉토리가 없으면 생성 (최초 배포 시)
176-
cd "${{ env.DEPLOY_DIR }}"
206+
cd "$DEPLOY_DIR"
177207
178208
# 2) EC2에 미리 만들어둔 pinback.env 파일의 환경 변수 로드
179-
if [ ! -f "${{ env.DEPLOY_DIR }}/pinback.env" ]; then
180-
echo "ERROR: .env file not found at ${{ env.DEPLOY_DIR }}/pinback.env"
209+
if [ ! -f "$DEPLOY_DIR/pinback.env" ]; then
210+
echo "ERROR: .env file not found at $DEPLOY_DIR/pinback.env"
181211
exit 1
182212
fi
183-
export $(cat "${{ env.DEPLOY_DIR }}/pinback.env" | xargs) && \
213+
while IFS='=' read -r name value; do
214+
# 주석 및 빈 줄 무시
215+
if [[ "$name" =~ ^# ]] || [ -z "$name" ]; then
216+
continue
217+
fi
218+
# 공백 제거 및 export
219+
export "$name"="$(echo "$value" | xargs)"
220+
done < "$DEPLOY_DIR/pinback.env"
184221
185-
# 3) 기존 Docker Compose 스택 중지 및 삭제 (있다면)
186-
docker compose -f "${{ env.DEPLOY_DIR }}/docker-compose.yml" down || true && \
222+
# 3) 기존 Docker Compose 스택 중지 및 삭제
223+
docker-compose --file "$DEPLOY_DIR/docker-compose.yml" down || true && \
187224
188225
# Docker Hub에서 최신 이미지 pull
189-
docker compose -f "${{ env.DEPLOY_DIR }}/docker-compose.yml" pull && \
226+
docker-compose --file "$DEPLOY_DIR/docker-compose.yml" pull && \
190227
191228
# 5) Docker Compose로 서비스 시작
192-
docker compose -f "${{ env.DEPLOY_DIR }}/docker-compose.yml" up -d --remove-orphans && \
229+
docker-compose --file "$DEPLOY_DIR/docker-compose.yml" up -d --remove-orphans && \
193230
194231
# 6) 사용하지 않는 Docker 이미지 정리
195232
docker image prune -f

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ COPY . .
66

77
RUN ./gradlew clean build -x test
88

9-
FROM openjdk:21-jdk-slim AS runtime
9+
FROM eclipse-temurin:21-jre-jammy AS runtime
1010

1111
COPY --from=builder /app/api/build/libs/*.jar /pinback.jar
1212

api/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ dependencies {
2020
implementation 'io.jsonwebtoken:jjwt-impl:0.12.6'
2121
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.6'
2222

23+
implementation 'org.springframework.boot:spring-boot-starter-webflux'
24+
2325
runtimeOnly 'com.mysql:mysql-connector-j'
2426
runtimeOnly 'com.h2database:h2'
2527

api/src/main/java/com/pinback/api/PinbackApiApplication.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
55
import org.springframework.boot.autoconfigure.domain.EntityScan;
6+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
67
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
78
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
89

10+
import com.pinback.application.config.ProfileImageConfig;
11+
912
@SpringBootApplication(scanBasePackages = {
1013
"com.pinback.api",
1114
"com.pinback.application",
@@ -15,6 +18,7 @@
1518
@EntityScan("com.pinback.domain")
1619
@EnableJpaRepositories("com.pinback.infrastructure")
1720
@EnableJpaAuditing
21+
@EnableConfigurationProperties(ProfileImageConfig.class)
1822
public class PinbackApiApplication {
1923
public static void main(String[] args) {
2024
SpringApplication.run(PinbackApiApplication.class, args);
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.pinback.api.article.controller;
2+
3+
import java.time.LocalDateTime;
4+
5+
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.PatchMapping;
7+
import org.springframework.web.bind.annotation.PathVariable;
8+
import org.springframework.web.bind.annotation.RequestMapping;
9+
import org.springframework.web.bind.annotation.RequestParam;
10+
import org.springframework.web.bind.annotation.RestController;
11+
12+
import com.pinback.application.article.dto.query.PageQuery;
13+
import com.pinback.application.article.dto.response.ReadRemindArticleResponse;
14+
import com.pinback.application.article.dto.response.TodayRemindResponseV2;
15+
import com.pinback.application.article.port.in.GetArticlePort;
16+
import com.pinback.application.article.port.in.UpdateArticleStatusPort;
17+
import com.pinback.domain.user.entity.User;
18+
import com.pinback.shared.annotation.CurrentUser;
19+
import com.pinback.shared.dto.ResponseDto;
20+
21+
import io.swagger.v3.oas.annotations.Operation;
22+
import io.swagger.v3.oas.annotations.Parameter;
23+
import io.swagger.v3.oas.annotations.tags.Tag;
24+
import lombok.RequiredArgsConstructor;
25+
import lombok.extern.slf4j.Slf4j;
26+
27+
@Slf4j
28+
@RestController
29+
@RequestMapping("/api/v2/articles")
30+
@RequiredArgsConstructor
31+
@Tag(name = "ArticleV2", description = "아티클 관리 API V2")
32+
public class ArticleControllerV2 {
33+
private final GetArticlePort getArticlePort;
34+
private final UpdateArticleStatusPort updateArticleStatusPort;
35+
36+
@Operation(summary = "리마인드 아티클 조회 v2", description = "오늘 리마인드할 아티클을 읽음/안읽음 상태별로 조회합니다.")
37+
@GetMapping("/remind")
38+
public ResponseDto<TodayRemindResponseV2> getRemindArticlesV2(
39+
@Parameter(hidden = true) @CurrentUser User user,
40+
@Parameter(description = "현재 시간", example = "2025-09-03T10:00:00") @RequestParam LocalDateTime now,
41+
@Parameter(description = "읽음 상태 (true: 읽음, false: 안읽음)", example = "true") @RequestParam(name = "read-status") boolean readStatus,
42+
@Parameter(description = "페이지 번호 (0부터 시작)") @RequestParam(defaultValue = "0") int page,
43+
@Parameter(description = "페이지 크기") @RequestParam(defaultValue = "8") int size
44+
) {
45+
PageQuery query = new PageQuery(page, size);
46+
TodayRemindResponseV2 response = getArticlePort.getRemindArticlesV2(user, now, readStatus, query);
47+
return ResponseDto.ok(response);
48+
}
49+
50+
@Operation(summary = "리마인드 아티클 읽음 상태 변경 v2", description = "리마인드 아티클의 읽음 상태를 변경합니다")
51+
@PatchMapping("/remind/{articleId}/read-status")
52+
public ResponseDto<ReadRemindArticleResponse> updateRemindArticleStatus(
53+
@Parameter(hidden = true) @CurrentUser User user,
54+
@Parameter(description = "아티클 ID") @PathVariable Long articleId
55+
) {
56+
ReadRemindArticleResponse response = updateArticleStatusPort.updateRemindArticleStatus(user, articleId);
57+
return ResponseDto.ok(response);
58+
}
59+
60+
}

api/src/main/java/com/pinback/api/config/filter/JwtAuthenticationFilter.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,12 @@ protected boolean shouldNotFilter(HttpServletRequest request) throws ServletExce
9696
path.startsWith("/v3/api-docs") ||
9797
path.startsWith("/docs") ||
9898
path.startsWith("/api/v1/test/push") ||
99-
path.startsWith("/api/v1/test/health")
99+
path.startsWith("/api/v1/test/health") ||
100+
path.startsWith("/api/v2/auth/google") ||
101+
path.startsWith("/oauth/callback") ||
102+
path.startsWith("/login/google") ||
103+
path.startsWith("/login/oauth2/code/google") ||
104+
path.startsWith("/api/v2/auth/signup")
100105
;
101106
}
102107
}

api/src/main/java/com/pinback/api/config/security/SecurityConfig.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,21 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
5252
).permitAll()
5353

5454
.requestMatchers(
55-
"/api/v1/auth/token"
55+
"/api/v1/auth/token",
56+
"/api/v2/auth/google",
57+
"/api/v2/auth/signup"
5658
).permitAll()
5759

5860
.requestMatchers(
5961
"/api/v1/test/*"
6062
).permitAll()
6163

64+
.requestMatchers(
65+
"/login/google",
66+
"/oauth/callback",
67+
"/login/oauth2/code/google"
68+
).permitAll()
69+
6270
.anyRequest().authenticated()
6371
)
6472
.formLogin(AbstractHttpConfigurer::disable)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.pinback.api.google.controller;
2+
3+
import org.springframework.web.bind.annotation.PatchMapping;
4+
import org.springframework.web.bind.annotation.PostMapping;
5+
import org.springframework.web.bind.annotation.RequestBody;
6+
import org.springframework.web.bind.annotation.RequestMapping;
7+
import org.springframework.web.bind.annotation.RestController;
8+
9+
import com.pinback.api.auth.dto.request.SignUpRequest;
10+
import com.pinback.api.google.dto.request.GoogleLoginRequest;
11+
import com.pinback.application.auth.dto.SignUpResponse;
12+
import com.pinback.application.auth.usecase.AuthUsecase;
13+
import com.pinback.application.google.dto.response.GoogleLoginResponse;
14+
import com.pinback.application.google.usecase.GoogleUsecase;
15+
import com.pinback.shared.dto.ResponseDto;
16+
17+
import io.swagger.v3.oas.annotations.Operation;
18+
import io.swagger.v3.oas.annotations.tags.Tag;
19+
import jakarta.validation.Valid;
20+
import lombok.RequiredArgsConstructor;
21+
import lombok.extern.slf4j.Slf4j;
22+
import reactor.core.publisher.Mono;
23+
24+
@Slf4j
25+
@RestController
26+
@RequestMapping("/api/v2/auth")
27+
@RequiredArgsConstructor
28+
@Tag(name = "Google", description = "구글 소셜 로그인 API")
29+
public class GoogleLonginController {
30+
31+
private final GoogleUsecase googleUsecase;
32+
private final AuthUsecase authUsecase;
33+
34+
@Operation(summary = "구글 소셜 로그인", description = "구글을 통한 소셜 로그인을 진행합니다")
35+
@PostMapping("/google")
36+
public Mono<ResponseDto<GoogleLoginResponse>> googleLogin(
37+
@Valid @RequestBody GoogleLoginRequest request
38+
) {
39+
return googleUsecase.getUserInfo(request.toCommand())
40+
.flatMap(googleResponse -> {
41+
return authUsecase.getInfoAndToken(googleResponse.email(), googleResponse.pictureUrl(),
42+
googleResponse.name())
43+
.map(loginResponse -> {
44+
return ResponseDto.ok(loginResponse);
45+
});
46+
});
47+
}
48+
49+
@Operation(summary = "신규 회원 온보딩", description = "신규 회원의 기본 정보를 등록합니다")
50+
@PatchMapping("/signup")
51+
public ResponseDto<SignUpResponse> signUpV2(
52+
@Valid @RequestBody SignUpRequest request
53+
) {
54+
SignUpResponse response = authUsecase.signUpV2(request.toCommand());
55+
return ResponseDto.ok(response);
56+
}
57+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.pinback.api.google.dto.request;
2+
3+
import com.pinback.application.google.dto.GoogleLoginCommand;
4+
5+
import jakarta.validation.constraints.NotNull;
6+
7+
public record GoogleLoginRequest(
8+
@NotNull(message = "인가 코드(code)는 비어있을 수 없습니다.")
9+
String code
10+
) {
11+
public GoogleLoginCommand toCommand() {
12+
return new GoogleLoginCommand(code);
13+
}
14+
}

0 commit comments

Comments
 (0)