File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
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)을 한 번에 모아 최신순으로 합쳐 돌려준다.
*
* <p>Mattermost 조회는 채널별로 개별 catch한다 - 한쪽 채널(혹은 Mattermost 서버 자체)이
* 실패해도 이미 DB에서 구한 단계·메모 이력까지 통째로 못 보여줄 이유가 없다.</p>
*/
@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<TimelineEvent> timeline(Long orgId) {
Organization org = orgMapper.findById(orgId);
if (org == null) {
throw new OrgNotFoundException("기관을 찾을 수 없습니다: " + orgId);
}
List<TimelineEvent> 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<TimelineEvent> fileEvents(String channelId, String channelLabel) {
if (channelId == null || channelId.isBlank()) {
return List.of();
}
try {
List<FileView> files = mattermost.collectChannelFiles(channelId);
List<TimelineEvent> 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;
}
}