Skip to content

Feat: Add Contest Thumbnail Upload API (#59)#60

Merged
Sunja-An merged 1 commit intodevelopfrom
feat/#59-contest-thumbnail-upload
Feb 8, 2026
Merged

Feat: Add Contest Thumbnail Upload API (#59)#60
Sunja-An merged 1 commit intodevelopfrom
feat/#59-contest-thumbnail-upload

Conversation

@Sunja-An
Copy link
Copy Markdown
Contributor

@Sunja-An Sunja-An commented Feb 8, 2026

Summary

  • Add a dedicated API endpoint for uploading contest thumbnail images
  • POST /api/contests/:id/thumbnail — handles R2 upload + contest DB record update in a single operation
  • Includes contest leader permission check

Changes

File Description
internal/contest/application/port/contest_storage_port.go New - Define ContestStoragePort interface
internal/contest/application/contest_service.go Add UploadThumbnail method + SetStoragePort setter
internal/contest/application/dto/contest_dto.go Add ThumbnailUploadResponse DTO
internal/contest/presentation/contest_controller.go Add UploadThumbnail handler + route registration
internal/storage/domain/storage.go Add UploadTypeContestThumbnail type and size constant
internal/storage/provider.go Expose StoragePort field in Dependencies
cmd/server.go Inject StoragePort into ContestService

API Spec

  • Endpoint: POST /api/contests/:id/thumbnail
  • Auth: Bearer Token (JWT)
  • Content-Type: multipart/form-data
  • Param: file (image, max 5MB, jpeg/png/webp)
  • Authorization: Contest Leader only
  • Response: { key, url, size, mime_type, uploaded_at }

Test plan

  • Verify successful upload via POST /api/contests/:id/thumbnail with a leader account
  • Verify 403 response when called by a non-leader account
  • Verify 400 response when no file is provided
  • Verify error response when file exceeds 5MB
  • Verify error response for disallowed MIME types
  • Verify old thumbnail is deleted from R2 when re-uploading

Closes #59

🤖 Generated with Claude Code

Add POST /api/contests/:id/thumbnail endpoint that uploads a thumbnail
image to R2 storage and updates the contest's thumbnail URL and
banner_key fields in a single operation. Only contest leaders can upload.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @Sunja-An, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 콘테스트 리더가 자신의 콘테스트에 썸네일 이미지를 업로드할 수 있도록 새로운 기능을 도입합니다. 이 기능은 R2 스토리지에 이미지를 안전하게 업로드하고, 관련 콘테스트 데이터베이스 레코드를 자동으로 업데이트하며, 적절한 사용자 권한 검사를 보장하여 이미지 관리 프로세스를 간소화합니다. 이를 통해 사용자 경험을 향상시키고 콘테스트 콘텐츠의 시각적 매력을 높이는 것을 목표로 합니다.

Highlights

  • 새로운 API 엔드포인트 추가: 콘테스트 썸네일 이미지를 업로드하기 위한 전용 API 엔드포인트 POST /api/contests/:id/thumbnail이 추가되었습니다.
  • R2 업로드 및 DB 업데이트 자동화: R2 스토리지로의 이미지 업로드와 콘테스트 DB 레코드 업데이트가 단일 API 호출로 자동 처리됩니다.
  • 권한 검사: 썸네일 업로드 시 콘테스트 리더 권한을 확인하는 로직이 포함되었습니다.
  • 스토리지 포트 인터페이스 도입: 스토리지 관련 작업을 추상화하기 위한 ContestStoragePort 인터페이스가 새로 정의되었습니다.
  • 서비스 계층 로직 구현: ContestServiceUploadThumbnail 메서드와 SetStoragePort setter가 추가되어 썸네일 업로드의 핵심 비즈니스 로직을 처리합니다.
Changelog
  • cmd/server.go
    • 스토리지 모듈의 StoragePortContestService에 주입하는 로직을 추가했습니다.
  • internal/contest/application/contest_service.go
    • 파일 업로드 및 스토리지 도메인 관련 패키지를 임포트했습니다.
    • ContestService 구조체에 storagePort 필드를 추가했습니다.
    • 외부에서 스토리지 포트를 설정할 수 있도록 SetStoragePort 메서드를 추가했습니다.
    • 콘테스트 썸네일 업로드, 권한 확인, 파일 유효성 검사, R2 업로드, 기존 썸네일 삭제 및 DB 업데이트를 처리하는 UploadThumbnail 메서드를 구현했습니다.
  • internal/contest/application/dto/contest_dto.go
    • 썸네일 업로드 후 응답 데이터를 정의하는 ThumbnailUploadResponse DTO 구조체를 추가했습니다.
  • internal/contest/application/port/contest_storage_port.go
    • 콘테스트 스토리지 작업을 위한 ContestStoragePort 인터페이스를 새로 정의했습니다.
  • internal/contest/presentation/contest_controller.go
    • POST /api/contests/:id/thumbnail 라우트를 등록했습니다.
    • 썸네일 업로드 요청을 처리하는 UploadThumbnail 핸들러 메서드를 추가하고 Swagger 문서 주석을 포함했습니다.
  • internal/storage/domain/storage.go
    • UploadType 상수에 UploadTypeContestThumbnail을 추가했습니다.
    • 콘테스트 썸네일의 최대 파일 크기(MaxContestThumbnailSize, 5MB) 상수를 추가했습니다.
    • getMaxSize 함수에 UploadTypeContestThumbnail에 대한 케이스를 추가하여 올바른 최대 크기를 반환하도록 했습니다.
  • internal/storage/provider.go
    • Dependencies 구조체에 StoragePort 필드를 추가하여 스토리지 포트를 외부로 노출했습니다.
    • ProvideStorageDependencies 함수에서 StoragePort를 반환하도록 수정했습니다.
Activity
  • Sunja-An님이 콘테스트 썸네일 업로드 API 추가 기능을 구현하는 초기 커밋을 생성했습니다.
  • PR 설명에는 상세한 요약, 변경 사항, API 사양 및 포괄적인 테스트 계획이 포함되어 있어 철저한 준비를 보여줍니다.
  • 이 PR은 이슈 #59와 연결되어 있습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@Sunja-An Sunja-An changed the title Feat: Contest Thumbnail 업로드 API 추가 (#59) Feat: Add Contest Thumbnail Upload API (#59) Feb 8, 2026
@Sunja-An Sunja-An changed the base branch from main to develop February 8, 2026 18:56
@Sunja-An Sunja-An merged commit a7eb7a3 into develop Feb 8, 2026
1 check passed
@Sunja-An Sunja-An deleted the feat/#59-contest-thumbnail-upload branch February 8, 2026 18:58
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This PR introduces a new API endpoint for uploading contest thumbnails. While a security audit identified no critical vulnerabilities, a minor functional discrepancy was noted regarding the reuse of the BannerKey field for thumbnails. The implementation could be improved by addressing potential race conditions and error handling issues in contest_service.go to prevent data inconsistency or orphaned files. Additionally, there is code duplication in the controller and naming issues that impact maintainability.

Comment on lines +520 to +539
contest, err := c.repository.GetContestById(contestId)
if err != nil {
return nil, err
}

// Delete old thumbnail from storage if exists
if contest.BannerKey != nil && *contest.BannerKey != "" {
go func() {
if delErr := c.storagePort.Delete(context.Background(), *contest.BannerKey); delErr != nil {
log.Printf("[UploadThumbnail] Failed to delete old thumbnail %s: %v", *contest.BannerKey, delErr)
}
}()
}

contest.Thumbnail = &url
contest.BannerKey = &key

if err := c.repository.UpdateContest(contest); err != nil {
return nil, err
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

여기에 잠재적인 경쟁 조건(race condition)이 있습니다. 동일한 대회에 대한 썸네일 업로드 요청 두 개가 동시에 들어오면, 두 요청 모두 동일한 이전 BannerKey를 읽을 수 있으며, 마지막 DB 업데이트가 이기게 되어 스토리지에 고아 파일(orphaned file)이 남을 수 있습니다. contest 객체에 대한 읽기-수정-쓰기 작업(GetContestById부터 UpdateContest까지)은 이를 방지하기 위해 데이터베이스 트랜잭션 내에서 원자적으로 수행되어야 합니다. 일반적으로 이는 행을 잠그기 위해 SELECT ... FOR UPDATE를 사용하는 것을 포함합니다.

Comment on lines +537 to +539
if err := c.repository.UpdateContest(contest); err != nil {
return nil, err
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

UpdateContest가 실패하면 새 썸네일은 이미 스토리지에 업로드되었지만 데이터베이스 레코드는 업데이트되지 않습니다. 이로 인해 스토리지에 고아 파일이 남게 됩니다. 스토리지 작업을 롤백하기 위해 새로 업로드된 파일을 삭제하여 이 경우를 처리해야 합니다.

if err := c.repository.UpdateContest(contest); err != nil {
		// Attempt to delete the newly uploaded file on DB update failure
		go func() {
			if delErr := c.storagePort.Delete(context.Background(), key); delErr != nil {
				log.Printf("[UploadThumbnail] Failed to delete orphaned thumbnail %s after DB error: %v", key, delErr)
			}
		}()
		return nil, err
	}

}

contest.Thumbnail = &url
contest.BannerKey = &key
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

BannerKey 필드가 썸네일의 키를 저장하는 데 사용되고 있습니다. 이 기능은 배너가 아닌 썸네일에 관한 것이므로 이는 오해의 소지가 있고 혼란을 야기할 수 있습니다. ThumbnailKey와 같이 더 적절한 이름을 사용하는 것이 좋습니다. 코드 명확성과 유지보수성을 향상시키기 위해 Contest 도메인 객체 및 이 필드가 사용되는 다른 모든 곳에서 필드 이름을 변경하는 것을 권장합니다.

Comment on lines +281 to +302
func (c *ContestController) UploadThumbnail(ctx *gin.Context) {
id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
if err != nil {
response.JSON(ctx, response.BadRequest("invalid contest id"))
return
}

userId, ok := middleware.GetUserIdFromContext(ctx)
if !ok {
response.JSON(ctx, response.Error(401, "user not authenticated"))
return
}

file, err := ctx.FormFile("file")
if err != nil {
response.JSON(ctx, response.BadRequest("file is required"))
return
}

result, err := c.service.UploadThumbnail(ctx.Request.Context(), id, userId, file)
c.helper.RespondCreated(ctx, result, err, "thumbnail uploaded successfully")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

UploadThumbnail 메소드의 로직은 파일 끝에 있는 HandleUploadThumbnail 함수와 중복됩니다. 이는 DRY(Don't Repeat Yourself) 원칙을 위반하며 코드를 유지보수하기 어렵게 만듭니다. 이 파일의 기존 패턴은 로직을 포함하는 '테스트 가능한' 핸들러 함수를 두고 컨트롤러 메소드가 이를 호출하는 것으로 보입니다. 중복을 제거하기 위해 이 패턴을 따르는 것이 좋습니다.

이 메소드를 HandleUploadThumbnail을 호출하도록 리팩토링하세요.

func (c *ContestController) UploadThumbnail(ctx *gin.Context) {
	HandleUploadThumbnail(ctx, c.service, c.helper)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Contest Thumbnail 업로드 API 추가

1 participant