package kr.itn.itnhub.timeline; import kr.itn.itnhub.feed.FileView; import kr.itn.itnhub.mattermost.MattermostClient; import kr.itn.itnhub.memo.WorkMemo; import kr.itn.itnhub.memo.WorkMemoMapper; import kr.itn.itnhub.org.OrgNotFoundException; import kr.itn.itnhub.org.Organization; import kr.itn.itnhub.org.OrganizationMapper; import kr.itn.itnhub.stage.StageHistoryMapper; import kr.itn.itnhub.stage.StageHistoryRow; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Comparator; import java.util.List; /** * 기관관리 상세의 타임라인 탭이 쓰는 서비스. 단계변경 이력(stage_history) + 업무메모(work_memo) + * 채널 첨부파일(Mattermost)을 한 번에 모아 최신순으로 합쳐 돌려준다. * *

Mattermost 조회는 채널별로 개별 catch한다 - 한쪽 채널(혹은 Mattermost 서버 자체)이 * 실패해도 이미 DB에서 구한 단계·메모 이력까지 통째로 못 보여줄 이유가 없다.

*/ @Service public class TimelineService { private static final Logger log = LoggerFactory.getLogger(TimelineService.class); private static final String LABEL_MJ = "문정원"; private static final String LABEL_LAW = "법률검토"; /** 메모 상세문(detail)로 보여줄 첫 줄의 최대 길이. */ private static final int MEMO_DETAIL_MAX_LEN = 60; private final OrganizationMapper orgMapper; private final StageHistoryMapper stageHistoryMapper; private final WorkMemoMapper memoMapper; private final MattermostClient mattermost; public TimelineService(OrganizationMapper orgMapper, StageHistoryMapper stageHistoryMapper, WorkMemoMapper memoMapper, MattermostClient mattermost) { this.orgMapper = orgMapper; this.stageHistoryMapper = stageHistoryMapper; this.memoMapper = memoMapper; this.mattermost = mattermost; } public List timeline(Long orgId) { Organization org = orgMapper.findById(orgId); if (org == null) { throw new OrgNotFoundException("기관을 찾을 수 없습니다: " + orgId); } List events = new ArrayList<>(); for (StageHistoryRow row : stageHistoryMapper.findByOrg(orgId)) { events.add(new TimelineEvent("STAGE", row.getChangedAt(), String.valueOf(row.getStage()), null)); } for (WorkMemo memo : memoMapper.findByOrg(orgId)) { events.add(new TimelineEvent("MEMO", memo.getCreatedAt(), memo.getContactName(), firstLine(memo.getBody()))); } events.addAll(fileEvents(org.getChannelIdMj(), LABEL_MJ)); events.addAll(fileEvents(org.getChannelIdLaw(), LABEL_LAW)); events.sort(Comparator.comparingLong(TimelineEvent::at).reversed()); return events; } private List fileEvents(String channelId, String channelLabel) { if (channelId == null || channelId.isBlank()) { return List.of(); } try { List files = mattermost.collectChannelFiles(channelId); List events = new ArrayList<>(files.size()); for (FileView file : files) { events.add(new TimelineEvent("FILE", file.createAt(), file.name(), channelLabel + " · " + file.uploader())); } return events; } catch (RuntimeException e) { log.warn("타임라인 자료 조회 실패 channel={}", channelLabel, e); return List.of(); } } private String firstLine(String body) { if (body == null) { return ""; } String line = body.strip().split("\\R", 2)[0]; if (line.length() > MEMO_DETAIL_MAX_LEN) { return line.substring(0, MEMO_DETAIL_MAX_LEN); } return line; } }