@@ -215,6 +215,68 @@ public String buildSummaryCsv(String date, TeamType teamOrNull) {
215215 return new String (sb .toString ().getBytes (StandardCharsets .UTF_8 ), StandardCharsets .UTF_8 );
216216 }
217217
218+ @ Transactional (readOnly = true )
219+ public String buildFullMatrixCsv (TeamType teamOrNull ) {
220+ // 1) 날짜 목록(오름차순)
221+ List <LocalDate > dates = meetingRepository
222+ .findAll (Sort .by (Sort .Direction .ASC , "meetingDate" ))
223+ .stream ()
224+ .map (Meeting ::getMeetingDate )
225+ .toList ();
226+
227+ // 날짜가 없으면 헤더만
228+ if (dates .isEmpty ()) {
229+ return "이름\n " ;
230+ }
231+
232+ // 2) 대상 사용자: 기존 정책과 동일 (CORE/LEAD/ORGANIZER), 팀 필터 적용
233+ var roles = List .of (UserRole .CORE , UserRole .LEAD , UserRole .ORGANIZER );
234+ List <User > users = (teamOrNull == null )
235+ ? userRepository .findByUserRoleIn (roles )
236+ : userRepository .findByTeamAndUserRoleIn (teamOrNull , roles );
237+
238+ // 팀 없는 사용자 제외 + 이름순 정렬
239+ users = users .stream ()
240+ .filter (u -> u .getTeam () != null )
241+ .sorted (Comparator .comparing (User ::getName ))
242+ .toList ();
243+
244+ // 사용자 없으면 헤더만
245+ if (users .isEmpty ()) {
246+ StringBuilder onlyHeader = new StringBuilder ("이름" );
247+ for (LocalDate d : dates ) onlyHeader .append (',' ).append (d );
248+ onlyHeader .append ('\n' );
249+ return new String (onlyHeader .toString ().getBytes (StandardCharsets .UTF_8 ), StandardCharsets .UTF_8 );
250+ }
251+
252+ // 3) 날짜별 출석 맵을 한 번에 수집 (N×M 쿼리 방지 필요시 Repository 확장 고려)
253+ // 여기서는 기존 getPresenceMap(LocalDate)를 재사용해 날짜 단위로 가져옴
254+ List <Map <Long , Boolean >> presenceByDate = new ArrayList <>(dates .size ());
255+ for (LocalDate d : dates ) {
256+ presenceByDate .add (getPresenceMap (d )); // userId -> present
257+ }
258+
259+ // 4) CSV 빌드
260+ StringBuilder sb = new StringBuilder ();
261+ // Header
262+ sb .append ("이름" );
263+ for (LocalDate d : dates ) sb .append (',' ).append (d );
264+ sb .append ('\n' );
265+
266+ // Rows
267+ for (User u : users ) {
268+ sb .append (escape (u .getName ()));
269+ Long uid = u .getId ();
270+ for (Map <Long , Boolean > day : presenceByDate ) {
271+ boolean present = Boolean .TRUE .equals (day .getOrDefault (uid , false ));
272+ sb .append (',' ).append (present ? 'O' : 'X' );
273+ }
274+ sb .append ('\n' );
275+ }
276+
277+ return new String (sb .toString ().getBytes (StandardCharsets .UTF_8 ), StandardCharsets .UTF_8 );
278+ }
279+
218280 /* ===================== helpers ===================== */
219281
220282 /** date로 meeting을 보장하고 meetingId 반환 */
0 commit comments