Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
02294a5
[feat] 한글 영어 변환기 의존성 추가 (#286)
hd0rable Aug 30, 2025
99072b2
[feat] 관련 에러코드 추가,삭제 (#286)
hd0rable Aug 30, 2025
898d69f
[feat]피드 생성시 이미지 업로드용 presigned url 발급 요청 컨트롤러 추가 (#286)
hd0rable Aug 30, 2025
1f8b39d
[refactor]피드 생성시 이미지 url로 요청받도록 수정 (#286)
hd0rable Aug 30, 2025
8d55e7b
[refactor]피드 생성시 이미지 url로 요청받도록 수정 (#286)
hd0rable Aug 30, 2025
31cb3c6
[refactor]피드 생성시 이미지 url로 요청받도록 수정 (#286)
hd0rable Aug 30, 2025
9d2329b
[refactor]피드 생성시 이미지 url로 요청받도록 수정 (#286)
hd0rable Aug 30, 2025
ce079e7
[refactor] s3CommandPort 삭제 (#286)
hd0rable Aug 30, 2025
1dd5af6
[refactor] 피드 생성시 이미지 업로드용 presigned url 발급 요청 S3Service 리팩토링 (#286)
hd0rable Aug 30, 2025
35bb345
[refactor] 관련 스웨거 에러코드 추가 (#286)
hd0rable Aug 30, 2025
3dd8bc6
[refactor] 피드 생성시 이미지 업로드용 presigned url 발급 요청 dto 작성 (#286)
hd0rable Aug 30, 2025
b2e5d20
[refactor] 피드 생성시 이미지 업로드용 presigned url 발급 요청 dto 작성 (#286)
hd0rable Aug 30, 2025
ba0c2b8
[refactor] 피드 생성 테스트코드 수정 (#286)
hd0rable Aug 30, 2025
27ef6a6
[refactor] 피드 생성 단위 테스트코드 수정 (#286)
hd0rable Aug 30, 2025
4325330
[remove] TestS3MockConfig 삭제(#286)
hd0rable Aug 30, 2025
d51471b
Merge remote-tracking branch 'origin/develop' into refactor/#286-s3-p…
hd0rable Aug 30, 2025
102db8c
머지 충돌 해결
hd0rable Aug 30, 2025
63e8531
[refactor] 테스트코드 수정 (#286)
hd0rable Aug 30, 2025
699414d
Merge remote-tracking branch 'origin/develop' into refactor/#286-s3-p…
hd0rable Aug 30, 2025
d4f542a
Merge remote-tracking branch 'origin/develop' into refactor/#286-s3-p…
hd0rable Sep 1, 2025
76081cc
[refactor] Transliterator 의존성 제거
hd0rable Sep 1, 2025
37a41f6
[refactor] 스웨거 관련 에러코드 추가(#286)
hd0rable Sep 1, 2025
370d7e9
[refactor] 피드 도메인 관련 주석 삭제 (#286)
hd0rable Sep 1, 2025
bfb8c77
[refactor] presigned url 생성시 디렉토리 구체화 (#286)
hd0rable Sep 1, 2025
97291a7
[refactor] 피드 생성 통합 테스트코드 수정 (#286)
hd0rable Sep 1, 2025
2656ffd
[refactor] 피드 생성 단위 테스트코드 수정 (#286)
hd0rable Sep 1, 2025
b6935c8
[refactor] 피드 생성시 이밎 url 검증 (#286)
hd0rable Sep 1, 2025
3b7a375
[refactor] 스웨거 에러코드 추가 (#286)
hd0rable Sep 1, 2025
8f5803e
[refactor] presigned url 생성시 디렉토리 구체화 (#286)
hd0rable Sep 1, 2025
cc97c23
[refactor] presigned url검증 헬퍼서비스 작성 (#286)
hd0rable Sep 1, 2025
d3a04ca
[refactor] dto 수정 (#286)
hd0rable Sep 1, 2025
c1390ea
[refactor] bucket-base-url을 cloud-front-base-url로 변경 (#286)
hd0rable Sep 1, 2025
d991e82
[refactor] bucket-base-url을 cloud-front-base-url로 변경 (#286)
hd0rable Sep 1, 2025
248f5ee
[refactor] 피드 생성 단위 테스트 코드 수정 (#286)
hd0rable Sep 1, 2025
25a9738
[refactor] url 변경 (#286)
hd0rable Sep 1, 2025
3577d94
[refactor] npe 방지 (#286)
hd0rable Sep 1, 2025
e16a57b
Merge remote-tracking branch 'origin/develop' into refactor/#286-s3-p…
hd0rable Sep 1, 2025
c04be0a
Merge remote-tracking branch 'origin/develop' into refactor/#286-s3-p…
hd0rable Sep 2, 2025
88f92d7
머지 충돌 해결
hd0rable Sep 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ dependencies {
// Flyway
implementation "org.flywaydb:flyway-core"
implementation "org.flywaydb:flyway-mysql"

}

def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,10 @@ public enum ErrorCode implements ResponseCode {
/**
* 170000 : Image File error
*/
EMPTY_FILE_EXCEPTION(HttpStatus.BAD_REQUEST, 170001, "업로드하려는 이미지가 비어있습니다."),
EXCEPTION_ON_IMAGE_UPLOAD(HttpStatus.BAD_REQUEST, 170002, "이미지 업로드에 실패하였습니다."),
INVALID_FILE_EXTENSION(HttpStatus.BAD_REQUEST, 170003, "올바르지 않은 파일 형식입니다."),
IO_EXCEPTION_ON_IMAGE_DELETE(HttpStatus.BAD_REQUEST, 170004, "파일 삭제에 실패하였습니다."),
INVALID_FILE_EXTENSION(HttpStatus.BAD_REQUEST, 170001, "허용하지 않는 파일 확장자입니다."),
FILE_SIZE_OVERFLOW(HttpStatus.BAD_REQUEST, 170002, "파일 크기가 허용 범위를 초과했습니다."),
URL_INVALID_DOMAIN(HttpStatus.BAD_REQUEST, 170003, "잘못된 이미지 URL 형식입니다."),
URL_USER_ID_MISMATCH(HttpStatus.BAD_REQUEST, 170004, "URL의 사용자 ID가 요청 사용자와 일치하지 않습니다."),

/**
* 180000 : Post error
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package konkuk.thip.common.s3.service;

import konkuk.thip.common.annotation.application.HelperService;
import konkuk.thip.common.exception.BusinessException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;

import java.util.List;

import static konkuk.thip.common.exception.code.ErrorCode.URL_INVALID_DOMAIN;
import static konkuk.thip.common.exception.code.ErrorCode.URL_USER_ID_MISMATCH;

@HelperService
@RequiredArgsConstructor
public class ImageUrlValidationService {
Comment on lines +13 to +15
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다


@Value("${cloud.aws.s3.cloud-front-base-url}")
private String cloudFrontBaseUrl;

public void validateUrlDomainAndUser(List<String> imageUrls, Long currentUserId) {

if (imageUrls == null || imageUrls.isEmpty()) {
return;
}

for (String url : imageUrls) {
// 1. 도메인 시작 주소 확인
if (!url.startsWith(cloudFrontBaseUrl)) {
throw new BusinessException(URL_INVALID_DOMAIN);
}
// 2. 도메인 이후의 key 추출
String key = url.substring(cloudFrontBaseUrl.length());
// 3. key를 '/'로 분리
if (key.startsWith("/")) {
key = key.substring(1); // 맨 앞 슬래시 제거
}
String[] parts = key.split("/");

// 4. 경로 구조 확인
// feed/{userId}/{date}/{uuid + filename}
if (parts.length != 4) {
throw new BusinessException(URL_INVALID_DOMAIN);
}

// 5. userId와 비교
String userIdInUrl = parts[1];
try {
Long userIdFromUrl = Long.parseLong(userIdInUrl);
if (!userIdFromUrl.equals(currentUserId)) {
throw new BusinessException(URL_USER_ID_MISMATCH);
}
} catch (NumberFormatException e) {
throw new BusinessException(URL_USER_ID_MISMATCH);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,14 +190,16 @@ public enum SwaggerResponseDescription {
BOOK_NOT_FOUND,
TAG_NOT_FOUND,
TAG_NAME_NOT_MATCH,
CONTENT_LIST_SIZE_OVERFLOW,
TAG_SHOULD_BE_UNIQUE,
TAG_LIST_SIZE_OVERFLOW,
BOOK_NAVER_API_PARSING_ERROR,
BOOK_NAVER_API_ISBN_NOT_FOUND,
EMPTY_FILE_EXCEPTION,
EXCEPTION_ON_IMAGE_UPLOAD,
INVALID_FILE_EXTENSION
URL_INVALID_DOMAIN,
URL_USER_ID_MISMATCH
))),
FEED_IMAGE_UPLOAD(new LinkedHashSet<>(Set.of(
INVALID_FILE_EXTENSION,
FILE_SIZE_OVERFLOW,
CONTENT_LIST_SIZE_OVERFLOW
))),
FEED_UPDATE(new LinkedHashSet<>(Set.of(
USER_NOT_FOUND,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ public class AwsS3ImageUrlInitializer {
private final AwsS3Properties awsS3Properties;

@PostConstruct
void bindBucketBaseUrl() {
String baseUrl = awsS3Properties.bucketBaseUrl();
void bindCloudFrontBaseUrl() {
String baseUrl = awsS3Properties.cloudFrontBaseUrl();
if (baseUrl == null || baseUrl.isEmpty()) {
throw new BusinessException(AWS_BUCKET_BASE_URL_NOT_CONFIGURED);
}

Alias.registerBaseUrlSupplier(awsS3Properties::bucketBaseUrl);
Category.registerBaseUrlSupplier(awsS3Properties::bucketBaseUrl);
Alias.registerBaseUrlSupplier(awsS3Properties::cloudFrontBaseUrl);
Category.registerBaseUrlSupplier(awsS3Properties::cloudFrontBaseUrl);
Comment on lines +26 to +27
Copy link

@coderabbitai coderabbitai bot Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

트레일링 슬래시를 정규화하지 않으면 '//' 이중 슬래시 URL이 생성될 수 있습니다.

등록 전 한 번 정규화해서 두 도메인 객체에 동일하게 주입하세요.

-        Alias.registerBaseUrlSupplier(awsS3Properties::cloudFrontBaseUrl);
-        Category.registerBaseUrlSupplier(awsS3Properties::cloudFrontBaseUrl);
+        String normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
+        Alias.registerBaseUrlSupplier(() -> normalizedBaseUrl);
+        Category.registerBaseUrlSupplier(() -> normalizedBaseUrl);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/config/AwsS3ImageUrlInitializer.java around lines
26-27, normalize the cloudFrontBaseUrl once before registering so neither Alias
nor Category receive a URL with a trailing slash (which can cause '//' when
concatenated); compute a single normalizedBaseUrl by trimming any trailing '/'
(and handling null/empty safely), then register suppliers that return this
normalizedBaseUrl for both Alias and Category so both use the exact same
sanitized value.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드래빗이 말하는 것처럼 현재 cloud-front-base-url의 환경변수 마지막이 슬래쉬(/)로 끝나고, Alias에서 사진 url의 시작이 슬래쉬(/)로 시작하는데 이중 슬래쉬 이슈는 없을까요??

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@buzz0331 좋은 지적입니다! 이중 슬래시 문제를 확인해보겠습니다.

네, 말씀하신 우려가 정확합니다!

검색 결과를 보면 AliasCategory 클래스에서 이미지 URL을 구성할 때 다음과 같은 패턴으로 사용하고 있습니다:

// Alias 클래스에서
return baseUrlSupplier.get() + "/user/" + userId + "/alias/" + value + ".jpg";

// Category 클래스에서  
return baseUrlSupplier.get() + "/room/" + value + ".jpg";

만약 cloud-front-base-url 환경변수가 https://cdn.example.com/처럼 슬래시로 끝나면:

  • https://cdn.example.com/ + /user/123/alias/image.jpg = https://cdn.example.com//user/123/alias/image.jpg

이렇게 이중 슬래시(//)가 발생합니다.

CodeRabbit의 제안이 정확합니다. 다음과 같이 수정하는 것이 좋겠습니다:

@PostConstruct
void bindCloudFrontBaseUrl() {
    String baseUrl = awsS3Properties.cloudFrontBaseUrl();
    if (baseUrl == null || baseUrl.isEmpty()) {
        throw new BusinessException(AWS_BUCKET_BASE_URL_NOT_CONFIGURED);
    }
    
    // 트레일링 슬래시 정규화
    String normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
    
    Alias.registerBaseUrlSupplier(() -> normalizedBaseUrl);
    Category.registerBaseUrlSupplier(() -> normalizedBaseUrl);
}

이렇게 하면 환경변수 설정과 관계없이 항상 올바른 URL이 생성됩니다.

  (\   /)
 ( ._. )  
o_(")(") 
*생각하는 토끼*

🏁 Script executed:

#!/bin/bash
# Alias 클래스에서 URL 구성 방식 확인
ast-grep --pattern $'class Alias {
  $$$
}'

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Alias와 Category 클래스에서 URL 구성 방식을 더 자세히 확인
rg -nP --type=java -C5 "registerBaseUrlSupplier|baseUrlSupplier" 

Length of output: 4903


🏁 Script executed:

#!/bin/bash
# baseUrl 사용하는 부분과 URL 구성 패턴 확인
rg -nP --type=java -C3 "baseUrl.*/" 

Length of output: 37


🏁 Script executed:

#!/bin/bash
# Alias 도메인 클래스에서 이미지 URL 생성 로직 확인
fd "Alias.java" --exec cat {}

Length of output: 1297


🏁 Script executed:

#!/bin/bash
# Category 도메인 클래스에서 이미지 URL 생성 로직 확인  
fd "Category.java" --exec cat {}

Length of output: 1649


@buzz0331 네, 정확히 맞습니다! 코드를 확인해보니 이중 슬래시 문제가 실제로 발생할 수 있습니다.

문제 상황 분석

Alias 클래스 (Line 40):

public String getImageUrl() {
   return baseUrlSupplier.get() + imageUrl;
}

Category 클래스 (Line 49):

public String getImageUrl() {
    return baseUrlSupplier.get() + imageUrl;
}

두 클래스 모두에서 imageUrl 값들이 슬래시로 시작합니다:

  • Alias: "/profile_literature.png", "/profile_science.png"
  • Category: "/group_science.png", "/group_literature.png"

만약 환경변수 cloud-front-base-urlhttps://cdn.example.com/처럼 슬래시로 끝나면:

"https://cdn.example.com/" + "/profile_literature.png" 
= "https://cdn.example.com//profile_literature.png"  // 이중 슬래시!

해결 방안

CodeRabbit의 제안대로 트레일링 슬래시를 정규화하는 것이 정확한 해결책입니다:

@PostConstruct
void bindCloudFrontBaseUrl() {
    String baseUrl = awsS3Properties.cloudFrontBaseUrl();
    if (baseUrl == null || baseUrl.isEmpty()) {
        throw new BusinessException(AWS_BUCKET_BASE_URL_NOT_CONFIGURED);
    }
    
    // 트레일링 슬래시 정규화 - 이중 슬래시 방지
    String normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
    
    Alias.registerBaseUrlSupplier(() -> normalizedBaseUrl);
    Category.registerBaseUrlSupplier(() -> normalizedBaseUrl);
}

이렇게 수정하면 환경변수 설정과 관계없이 항상 올바른 URL이 생성됩니다:

  • https://cdn.example.com + /profile_literature.png = https://cdn.example.com/profile_literature.png

좋은 지적이었습니다! 🎯

  (\   /)
 ( ^.^ )  
o_(")(") 
*문제를 찾은 토끼*

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 아눃 환경변수 값에서 /뻇습니닿ㅎㅎ

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
@ConfigurationProperties(prefix = "cloud.aws.s3")
@Validated
public record AwsS3Properties(
// 스프링 컨텍스트 시작 시점에 bucket-base-url의 값 유효성 검사 실행
@NotBlank(message = "bucket-base-url 은 비어있을 수 없습니다.")
@URL(message = "bucket-base-url 의 형식이 올바르지 않습니다.")
String bucketBaseUrl
// 스프링 컨텍스트 시작 시점에 cloud-front-base-url의 값 유효성 검사 실행
@NotBlank(message = "cloud-front-base-url 은 비어있을 수 없습니다.")
@URL(message = "cloud-front-base-url 의 형식이 올바르지 않습니다.")
String cloudFrontBaseUrl
Comment on lines +11 to +14
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

설정 키 변경에 따른 하위 호환 및 문서/환경 변수 갱신 확인 필요.

기존 bucket-base-url → cloud-front-base-url로 바뀌었으므로 application.yml/운영 환경 변수/문서에서 누락된 레거시 키가 없는지 점검해 주세요. 필요 시 마이그레이션 가이드를 PR 설명에 명시해주세요.

아래 스크립트로 레거시 용례를 빠르게 확인할 수 있습니다.


🏁 Script executed:

#!/bin/bash
# 레거시 키/식별자 흔적 찾기
rg -n -C2 -S 'bucket-base-url|bucketBaseUrl|AWS_BUCKET_BASE_URL_NOT_CONFIGURED'
# 설정 파일에서 현재 키 사용 여부 확인
fd -a 'application*.yml' | xargs -I{} rg -n 'cloud\.aws\.s3\.(cloud-front-base-url|bucket-base-url)' {}

Length of output: 1786


애플리케이션 설정·문서·예외코드 레거시 키 갱신 필요

  • application*.yml(예: src/main/resources/application.yml)에서 기존
    cloud.aws.s3.bucket-base-url 제거 후
    cloud.aws.s3.cloud-front-base-url 추가 → 운영 환경 변수(CLOUD_AWS_S3_CLOUD_FRONT_BASE_URL)도 동기화
  • PR 설명에 “기존 bucket-base-urlcloud-front-base-url 매핑” 마이그레이션 가이드 명시
  • ErrorCode.java
    AWS_BUCKET_BASE_URL_NOT_CONFIGURED("aws s3 bucket base url 설정이 누락되었습니다.")
    상수명·메시지를
    AWS_CLOUD_FRONT_BASE_URL_NOT_CONFIGURED("aws s3 cloud-front base url 설정이 누락되었습니다.")
    등으로 업데이트해주세요.
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/config/properties/AwsS3Properties.java around lines
11-14, update project configuration keys and error constants to reflect the
rename from bucket-base-url to cloud-front-base-url: remove the old
application.yml key cloud.aws.s3.bucket-base-url and add
cloud.aws.s3.cloud-front-base-url (ensure environment variable
CLOUD_AWS_S3_CLOUD_FRONT_BASE_URL is set in deployment), update the PR
description to include a migration guide mapping the old key to the new key, and
rename the ErrorCode constant AWS_BUCKET_BASE_URL_NOT_CONFIGURED to
AWS_CLOUD_FRONT_BASE_URL_NOT_CONFIGURED (and update its message text
accordingly) plus refactor all usages/tests/config references to the new
constant and key names.

) { }
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,16 @@
import konkuk.thip.common.dto.BaseResponse;
import konkuk.thip.common.security.annotation.UserId;
import konkuk.thip.common.swagger.annotation.ExceptionDescription;
import konkuk.thip.feed.adapter.in.web.request.FeedCreateRequest;
import konkuk.thip.feed.adapter.in.web.request.FeedIsLikeRequest;
import konkuk.thip.feed.adapter.in.web.request.FeedIsSavedRequest;
import konkuk.thip.feed.adapter.in.web.request.FeedUpdateRequest;
import konkuk.thip.feed.adapter.in.web.request.*;
import konkuk.thip.feed.adapter.in.web.response.FeedIdResponse;
import konkuk.thip.feed.adapter.in.web.response.FeedIsLikeResponse;
import konkuk.thip.feed.adapter.in.web.response.FeedIsSavedResponse;
import konkuk.thip.feed.application.port.in.FeedCreateUseCase;
import konkuk.thip.feed.application.port.in.FeedDeleteUseCase;
import konkuk.thip.feed.application.port.in.FeedSavedUseCase;
import konkuk.thip.feed.application.port.in.FeedUpdateUseCase;
import konkuk.thip.feed.adapter.in.web.response.FeedUploadImagePresignedUrlResponse;
import konkuk.thip.feed.adapter.out.s3.S3Service;
import konkuk.thip.feed.application.port.in.*;
import konkuk.thip.post.application.port.in.PostLikeUseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

Expand All @@ -38,17 +33,30 @@ public class FeedCommandController {
private final PostLikeUseCase postLikeUseCase;
private final FeedDeleteUseCase feedDeleteUseCase;

private final S3Service s3Service;

@Operation(
summary = "피드 작성",
description = "사용자가 피드를 작성합니다."
)
@ExceptionDescription(FEED_CREATE)
@PostMapping("/feeds")
public BaseResponse<FeedIdResponse> createFeed(
@RequestPart("request") @Valid final FeedCreateRequest request,
@Parameter(description = "피드에 첨부할 이미지 파일들") @RequestPart(value = "images", required = false) final List<MultipartFile> images,
@RequestBody @Valid final FeedCreateRequest request,
@Parameter(hidden = true) @UserId final Long userId) {
return BaseResponse.ok(FeedIdResponse.of(feedCreateUseCase.createFeed(request.toCommand(userId))));
}

@Operation(
summary = "피드 생성시 이미지 업로드용 presigned url 발급 요청",
description = "S3에 프론트엔드가 직접 이미지를 업로드하기위한 presigned url를 발급 받습니다."
)
@ExceptionDescription(FEED_IMAGE_UPLOAD)
@PostMapping("/feeds/images/presigned-url")
public BaseResponse<FeedUploadImagePresignedUrlResponse> getPresignedUrls(
@RequestBody @Valid final List<FeedUploadImagePresignedUrlRequest> request,
@Parameter(hidden = true) @UserId final Long userId) {
return BaseResponse.ok(FeedIdResponse.of(feedCreateUseCase.createFeed(request.toCommand(userId),images)));
return BaseResponse.ok(s3Service.getPresignedUrl(request,userId));
}

@Operation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,18 @@ public record FeedCreateRequest(
Boolean isPublic,

@Schema(description = "피드에 추가할 태그들", example = "[\"한국소설\", \"외국소설\", \"시\"]")
List<String> tagList
List<String> tagList,

@Schema(description = "피드에 추가할 이미지들", example = "[\"https://img.domain.com/1.jpg\", \"https://img.domain.com/2.jpg\"]")
List<String> imageUrls
) {
public FeedCreateCommand toCommand(Long userId) {
return new FeedCreateCommand(
isbn,
contentBody,
isPublic,
tagList,
imageUrls,
userId
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package konkuk.thip.feed.adapter.in.web.request;


import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;

@Schema(description = "피드 생성시 이미지 업로드용 presigned url발급 요청 DTO")
public record FeedUploadImagePresignedUrlRequest(

@Schema(description = "파일 이름 (확장자명 포함)", example = "image1.png")
@NotBlank(message = "파일 이름은 필수입니다.")
String filename,

@Schema(description = "파일 확장자 [허용 확장자 jpg, jpeg, png, gif]", example = "png")
@NotBlank(message = "파일 확장자는 필수입니다.")
String extension,

@Schema(description = "파일 크기 (바이트) [허용 파일 최대 크기 5MB]", example = "102400")
@Positive(message = "파일 크기는 양수여야 합니다.")
long size
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package konkuk.thip.feed.adapter.in.web.response;

import io.swagger.v3.oas.annotations.media.Schema;

import java.util.List;

public record FeedUploadImagePresignedUrlResponse(
List<PresignedUrlInfo> presignedUrls
) {
public static FeedUploadImagePresignedUrlResponse of(List<PresignedUrlInfo> urls) {
return new FeedUploadImagePresignedUrlResponse(urls);
}

@Schema(description = "Presigned URL 및 파일 접근 URL 정보")
public record PresignedUrlInfo(

@Schema(description = "PUT 요청을 보내기 위한 파일 업로드용 Presigned URL")
String presignedUrl,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻


@Schema(description = "업로드 후 접근 가능한 파일 URL")
String fileUrl
Comment on lines +20 to +21
Copy link
Collaborator

@seongjunnoh seongjunnoh Aug 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3 : 응답에 업로드 이후 접근 가능한 파일 url을 포함하신 이유가 있을까요??

FE에게 fileUrl 을 응답한 후, 다시 피드 생성 요청시에 fileUrl을 request body로 받는 것 보다는, FE에게 presignedUrl과 key(= presignedUrl을 생성할 때 사용한 key) 를 응답하고, 피드 생성 요청시에 key를 request body로 받아 내부적으로 key에 해당하는 fileUrl을 DB에 저장하는 플로우는 어떤가 싶습니다!!

현재 구현된 코드에서는 피드 생성 request 에 포함된 fileUrl 을 검증하는 부분이 누락되어 있는데, 위 방식으로 수정되면 내부에서 request 에 포함된 key에 해당하는 fileUrl 을 획득하는 과정에서 key 값의 검증과정이 포함되므로 더 안전할 것 같습니다!!

제가 생각한 피드 생성 플로우를 간략하게 추가해보았습니다!
[1] FE → BE: presign 요청(filename, size, type)
[2] BE → FE: { key, presignedPutUrl }
[3] FE → S3: PUT 업로드
[4] FE → BE: 피드 생성(content, imageKeys=[key1, key2, ...])
[5] BE: 각 key가 이 사용자 prefix인지 검증 + (선택) S3 HEAD로 존재/사이즈 확인
[6] BE: DB에는 s3Key 저장 (전체 cdnUrl을 저장하는게 아니라)
[7] 피드 조회 시 BE가 cdnUrl = cdnBase + key 로 조립해서 내려줌 -> 이러면 추후에 cdnBase 값이 수정되더라도 유연하게 대처 가능

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

굿굿 깔끔하게 정리해주셨네요! 성준님 말대로 접근 가능한 파일 url이 노출될 필요는 없어보이네요!

그리고 앞 리뷰에서 key를 구조화 시키면 성준님이 작성하신 것 중 5번에 해당하는 사용자 prefix(userId) 검증도 가능할 것 같습니다!

) {}
}

This file was deleted.

Loading