UserIconService.java

package com.hwhub.backend.application.service;

import com.hwhub.backend.domain.enums.ProgramType;
import com.hwhub.backend.domain.model.UserModel;
import com.hwhub.backend.domain.repository.UserRepository;
import com.hwhub.backend.domain.storage.ObjectStorageClient;
import com.hwhub.backend.infrastructure.s3.ObjectStorageConfig;
import java.net.URL;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class UserIconService {

  private final UserRepository userRepository;
  private final ObjectStorageClient objectStorageClient;
  private final ObjectStorageConfig.UserIconStorageSettings storageSettings;

  /**
   * ユーザのプロフィール画像upload用のpresigned URLとキーを返す。 画像uploadまでは3段階ある。
   *
   * <ol>
   *   <li>presigned URL取得 ★当メソッドがこれ
   *   <li>presigned URLを利用しファイルをupload(フロントからAPIをコールすること)
   *   <li>uploadした画像のキーをユーザマスタに登録
   * </ol>
   *
   * @param userId ユーザID
   * @param fileName uploadするファイルの名前
   * @param mimeType MIME type
   * @return 画像upload用のpresigned URL
   */
  public CreateIconUploadUrlResult createUploadUrl(Long userId, String fileName, String mimeType) {

    UserModel user =
        userRepository
            .findById(userId)
            .orElseThrow(() -> new IllegalStateException("user not found"));

    String ext = extractExtension(fileName);
    String key = buildFileKey(user.getUserId(), ext);

    URL url =
        objectStorageClient.createPresignedPutUrl(
            storageSettings.bucket(), key, mimeType, storageSettings.urlTtl());

    return new CreateIconUploadUrlResult(url.toString(), key);
  }

  /**
   * iconのキーを登録する。
   *
   * <ol>
   *   <li>presigned URL取得
   *   <li>presigned URLを利用しファイルをupload(フロントからAPIをコールすること)
   *   <li>uploadした画像のキーをユーザマスタに登録 ★当メソッドがこれ
   * </ol>
   *
   * @param userId ユーザID
   * @param fileKey uploadした画像のキー(#createUploadUrl()で生成されたキー)
   */
  public void updateUserIcon(Long userId, String fileKey) {
    UserModel user =
        userRepository
            .findById(userId)
            .orElseThrow(() -> new IllegalStateException("user not found"));

    String oldKey = user.getProfileImageKey();
    // 古いアイコンがあれば S3 から削除(失敗しても本処理は続行
    if (oldKey != null && !oldKey.equals(fileKey)) {
      try {
        objectStorageClient.deleteObject(storageSettings.bucket(), oldKey);
      } catch (Exception e) {
        // 処理継続
      }
    }

    user.changeProfileImageKey(fileKey);
    userRepository.updateProfileImgKey(user, ProgramType.ONL_USRICON.getCode());
  }

  /**
   * プロフィール画像をフロントからアクセスするためのURLを返す。
   *
   * @param userId ユーザID
   * @return プロフィール画像のURL
   */
  @Transactional(readOnly = true)
  public String getIconUrl(Long userId) {
    UserModel user =
        userRepository
            .findById(userId)
            .orElseThrow(() -> new IllegalStateException("user not found"));

    return getIconUrl(user.getProfileImageKey());
  }

  /**
   * プロフィール画像をフロントからアクセスするためのURLを返す。
   *
   * @param fileKey プロフィール画像のキー
   * @return プロフィール画像のURL
   */
  public String getIconUrl(String fileKey) {
    if (fileKey == null || fileKey.isBlank()) {
      return null;
    }

    URL url =
        objectStorageClient.createPresignedGetUrl(
            storageSettings.bucket(), fileKey, storageSettings.urlTtl());
    return url.toString();
  }

  // Presentation層に渡すDTO
  public record CreateIconUploadUrlResult(String uploadUrl, String fileKey) {}

  /**
   * プロフィール画像のキーを生成する。<br>
   * user-icon/{$userId}/icon{$ext}形式。ex) user-icon/10/icon.jpg
   *
   * @param userId ユーザID
   * @param ext 拡張子
   * @return プロフィール画像のキー
   */
  private String buildFileKey(Long userId, String ext) {
    // user-icon/{userId}/icon.jpg のイメージ
    return "%s/%d/icon%s".formatted(storageSettings.userIconBasePath(), userId, ext);
  }

  /**
   * 指定されたファイル名の拡張子を返す。
   *
   * @param fileName ファイル名
   * @return 拡張子。fileNameに拡張子がない場合は空文字を返却する。
   */
  private String extractExtension(String fileName) {
    if (fileName == null) return "";
    int dot = fileName.lastIndexOf('.');
    if (dot == -1) return "";
    return fileName.substring(dot); // ".jpg" など
  }
}