Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ public ResponseEntity<ApplicationFormResponseDto> joinRequest(

/**
* 해당 그룹 가입 신청 리스트 조회
* todo 유저 닉네임 추가
* @param userId
* @param groupId
* @return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import java.util.List;

import flipnote.group.application.port.in.result.FindMyJoinListResult;
import flipnote.group.domain.model.join.JoinInfo;
import flipnote.group.domain.model.join.JoinMyInfo;

public record FindMyJoinListResponseDto(
List<JoinInfo> joinList
List<JoinMyInfo> joinList
) {
public static FindMyJoinListResponseDto from(FindMyJoinListResult result) {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
package flipnote.group.application.port.in.result;

import java.util.List;
import java.util.Map;

import flipnote.group.adapter.out.entity.JoinEntity;
import flipnote.group.domain.model.join.JoinInfo;
import flipnote.user.grpc.GetUserResponse;

public record FindJoinFormListResult(
List<JoinInfo> joinInfoList
) {
public static FindJoinFormListResult of(List<JoinEntity> joinDomainList) {
public static FindJoinFormListResult of(List<JoinEntity> joinList, Map<Long, GetUserResponse> userMap) {

List<JoinInfo> joinInfoList = joinDomainList.stream()
.map(JoinInfo::of)
List<JoinInfo> results = joinList.stream()
.map(join -> {
GetUserResponse user = userMap.get(join.getUserId());
return JoinInfo.of(join, user.getNickname());
})
.toList();

return new FindJoinFormListResult(joinInfoList);
return new FindJoinFormListResult(results);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import java.util.List;

import flipnote.group.adapter.out.entity.JoinEntity;
import flipnote.group.domain.model.join.JoinInfo;
import flipnote.group.domain.model.join.JoinMyInfo;

public record FindMyJoinListResult(
List<JoinInfo> joinList
List<JoinMyInfo> joinList
) {
public static FindMyJoinListResult of(List<JoinEntity> joinList) {
List<JoinInfo> joinInfoList = joinList.stream()
.map(JoinInfo::of)
List<JoinMyInfo> joinInfoList = joinList.stream()
.map(JoinMyInfo::of)
.toList();
return new FindMyJoinListResult(joinInfoList);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package flipnote.group.application.service;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -25,6 +27,10 @@
import flipnote.group.domain.model.permission.GroupPermission;
import flipnote.group.domain.policy.BusinessException;
import flipnote.group.domain.policy.ErrorCode;
import flipnote.user.grpc.GetUserResponse;
import flipnote.user.grpc.GetUsersRequest;
import flipnote.user.grpc.GetUsersResponse;
import flipnote.user.grpc.UserQueryServiceGrpc;
import lombok.RequiredArgsConstructor;

@Service
Expand All @@ -35,6 +41,7 @@ public class ApplicationFormService implements JoinUseCase {
private final JoinRepositoryPort joinRepository;
private final GroupMemberRepositoryPort groupMemberRepository;
private final GroupRoleRepositoryPort groupRoleRepository;
private final UserQueryServiceGrpc.UserQueryServiceBlockingStub userQueryServiceStub;

private static final GroupPermission JOIN_MANAGE = GroupPermission.JOIN_REQUEST_MANAGE;

Expand Down Expand Up @@ -90,10 +97,25 @@ public FindJoinFormListResult findJoinFormList(FindJoinFormCommand cmd) {
throw new BusinessException(ErrorCode.PERMISSION_DENIED);
}

List<JoinEntity> joinDomainList = joinRepository.findFormList(cmd.groupId());
List<JoinEntity> joinList = joinRepository.findFormList(cmd.groupId());

// userId 목록 추출
List<Long> userIds = joinList.stream()
.map(JoinEntity::getUserId)
.toList();

return FindJoinFormListResult.of(joinDomainList);
// gRPC로 유저 정보 한번에 조회
GetUsersResponse usersResponse = userQueryServiceStub.getUsers(
GetUsersRequest.newBuilder()
.addAllUserIds(userIds)
.build()
);

// userId -> UserResponse 맵핑
Map<Long, GetUserResponse> userMap = usersResponse.getUsersList().stream()
.collect(Collectors.toMap(GetUserResponse::getId, u -> u));

return FindJoinFormListResult.of(joinList, userMap);
}

private void checkJoinable(GroupEntity group) {
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/flipnote/group/domain/model/join/JoinInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public record JoinInfo(
String joinIntro,
JoinStatus status
) {
public static JoinInfo of(JoinEntity join) {
return new JoinInfo(join.getId(), join.getUserId(), "nickname", join.getForm(), join.getStatus());
public static JoinInfo of(JoinEntity join, String nickname) {
return new JoinInfo(join.getId(), join.getUserId(), nickname, join.getForm(), join.getStatus());
}
}
13 changes: 13 additions & 0 deletions src/main/java/flipnote/group/domain/model/join/JoinMyInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package flipnote.group.domain.model.join;

import flipnote.group.adapter.out.entity.JoinEntity;

public record JoinMyInfo(
Long groupJoinId,
String joinIntro,
JoinStatus status
) {
public static JoinMyInfo of(JoinEntity join) {
return new JoinMyInfo(join.getId(), join.getForm(), join.getStatus());
}
}
17 changes: 17 additions & 0 deletions src/main/java/flipnote/group/global/config/GrpcClientConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.context.annotation.Configuration;

import flipnote.image.grpc.v1.ImageCommandServiceGrpc;
import flipnote.user.grpc.UserQueryServiceGrpc;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

Expand All @@ -26,4 +27,20 @@ public ImageCommandServiceGrpc.ImageCommandServiceBlockingStub imageCommandServi
) {
return ImageCommandServiceGrpc.newBlockingStub(imageCommandChannel);
}

@Bean(destroyMethod = "shutdownNow")
public ManagedChannel userQueryChannel(
@Value("${spring.grpc.user.address:localhost:9091}") String target
) {
return ManagedChannelBuilder.forTarget(target)
.usePlaintext()
.build();
}

@Bean
public UserQueryServiceGrpc.UserQueryServiceBlockingStub userQueryServiceStub(
ManagedChannel userQueryChannel
) {
return UserQueryServiceGrpc.newBlockingStub(userQueryChannel);
}
}
31 changes: 31 additions & 0 deletions src/main/proto/user.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
syntax = "proto3";

option java_package = "flipnote.user.grpc";
option java_outer_classname = "UserQueryProto";
option java_multiple_files = true;

package user_query;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# First, check if proto directory exists and list files
echo "=== Proto files structure ==="
if [ -d "src/main/proto" ]; then
    find src/main/proto -type f -name "*.proto" | head -20
else
    echo "Directory src/main/proto not found"
fi

echo ""
echo "=== Running package-directory mismatch check ==="
python3 <<'PY'
from pathlib import Path

root = Path("src/main/proto")
if not root.exists():
    print(f"Root path {root} does not exist")
else:
    violations = []
    for p in sorted(root.rglob("*.proto")):
        pkg = None
        try:
            content = p.read_text(encoding="utf-8")
            for line in content.splitlines():
                s = line.strip()
                if s.startswith("package ") and s.endswith(";"):
                    pkg = s[len("package "):-1].strip()
                    break
        except Exception as e:
            print(f"Error reading {p}: {e}")
            continue
        
        if not pkg:
            print(f"No package found in {p}")
            continue
        
        expected_dir = root / pkg.replace(".", "/")
        if p.parent != expected_dir:
            violations.append({
                'file': str(p),
                'package': pkg,
                'expected_dir': str(expected_dir),
                'actual_dir': str(p.parent)
            })
    
    if violations:
        print(f"Found {len(violations)} PACKAGE_DIRECTORY_MATCH violation(s):")
        for v in violations:
            print(f"  - File: {v['file']}")
            print(f"    Package: {v['package']}")
            print(f"    Expected dir: {v['expected_dir']}")
            print(f"    Actual dir: {v['actual_dir']}")
    else:
        print("No PACKAGE_DIRECTORY_MATCH violations found")
PY

Repository: FlipNoteTeam/FlipNote-Group

Length of output: 675


🏁 Script executed:

cat src/main/proto/user.proto

Repository: FlipNoteTeam/FlipNote-Group

Length of output: 682


package 선언과 파일 경로가 맞지 않아 proto lint가 실패합니다.

package user_query;면 Buf 규칙상 파일은 src/main/proto/user_query/ 디렉토리 하위에 있어야 합니다. 현재 위치(src/main/proto/user.proto)는 PACKAGE_DIRECTORY_MATCH 오류를 유발합니다. 파일을 src/main/proto/user_query/user.proto로 이동하거나 패키지 선언을 수정하세요.

🧰 Tools
🪛 Buf (1.65.0)

[error] 7-7: Files with package "user_query" must be within a directory "user_query" relative to root but were in directory "src/main/proto".

(PACKAGE_DIRECTORY_MATCH)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/proto/user.proto` at line 7, The proto package declaration "package
user_query;" in user.proto doesn't match its repository location and triggers
PACKAGE_DIRECTORY_MATCH linting; fix by either moving the user.proto file into a
directory that corresponds to the package namespace (so its filesystem path
matches package user_query) or by changing the package declaration in user.proto
from "user_query" to the correct package name that matches the file's current
directory; ensure the package token exactly matches the target directory name
and re-run the proto linter.


service UserQueryService {
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc GetUsers(GetUsersRequest) returns (GetUsersResponse);
}

message GetUserRequest {
int64 user_id = 1;
}

message GetUserResponse {
int64 id = 1;
string email = 2;
string nickname = 3;
string profile_image_url = 4;
}
Comment on lines +18 to +23
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

현재 사용 목적 대비 응답 스키마가 과도하게 넓습니다.

닉네임 매핑 용도인데 email까지 함께 전달되어 서비스 간 PII 노출면이 커집니다. id, nickname 중심의 축소 응답(또는 별도 RPC)으로 분리하는 편이 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/proto/user.proto` around lines 18 - 23, The GetUserResponse message
currently exposes unnecessary PII (email); update the proto to tighten the
response for nickname mapping by removing the email and profile_image_url fields
and keeping only id and nickname in GetUserResponse, or alternatively create a
new lightweight RPC/message (e.g., GetUserNicknameResponse) that contains just
int64 id and string nickname for the mapping use-case; update any references to
GetUserResponse in server handlers and clients to use the new/trimmed message to
avoid leaking email across services.


message GetUsersRequest {
repeated int64 user_ids = 1;
}

message GetUsersResponse {
repeated GetUserResponse users = 1;
}
28 changes: 17 additions & 11 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ spring:
port: 9094
image:
address: ${GRPC_IMAGE_URL:localhost:9092}
user:
address: ${GRPC_USER_URL:localhost:9091}

datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
Expand All @@ -30,17 +32,21 @@ springdoc:
api-docs:
path: /groups/v3/api-docs


image-clean:
batch-size: 300
orphan-grace-minutes: 10

image:
default:
user: https://flipnote-bucket.s3.ap-northeast-2.amazonaws.com/image/default/user.png
group: https://flipnote-bucket.s3.ap-northeast-2.amazonaws.com/image/default/group.png
cardSet: https://flipnote-bucket.s3.ap-northeast-2.amazonaws.com/image/default/cardset.png

logging:
level:
flipnote.group: DEBUG

management:
endpoints:
web:
exposure:
include: health
endpoint:
health:
probes:
enabled: true
health:
livenessState:
enabled: true
readinessState:
enabled: true