~완성화면 미리보기~


일기 생성 기능 개발
01. 구상
25.01.14
지도기능으로 산책 경로까지 추가하니까... 뭔가 하루에 있었던 일을 요약할 겸 일기가 자동으로 생성됐으면 좋겠다는 생각이 들었다. 그래서 이번에는 gpt등 NLP 모델을 이용해서 체크리스트 완료 여부, 이동했던 위치를 받으면 NLP가 그에 맞게 일기를 생성해 주는 걸 도전해 보기로 했다!
02. Hugging Face 모델 키 발급
간단한 토이 프로젝트니까 무료 모델을 쓰려고 했는데, 찾아보니 아래 사이트에서 모델들을 고를 수 있다길래 여기로 회원가입을 해서 키를 발급받았다.
Hugging Face – The AI community building the future.
huggingface.co
setting -> Access Tokens -> create new Access token -> Read(필요에 따라 선택 가능, 주로 read만 사용)
여기서 Token name은 그냥 아무거나... 난 프로젝트 기능 관련 이름으로, diary-generator로 하였다.

Create Token 버튼을 누르면 키가 나온다. 이걸 application.properties에 추가해 준다.
여기서 사용할 모델의 링크도 같이 써주면 된다. (+아래 gpt2같이 무료버전을 써봤는데... 이따 결과도 말하겠지만 진짜 텍스트 생성에는 쓸 게 못 된다............)

03. 코드 작성 (NLP 무료 버전 이용 - 실패)
저번에 지도 위치 추가하는 건 새로고침해도 저장했던 정보가 남아있어야 하기 때문에 데이터베이스가 필요했다. 그래서 domain에서 Entity부터 추가하였었다. 하지만 일기는 자동으로 생성해 주기 때문에 사용자랑 상관이 없고, 딱히 저장할 필요 없이 그냥 새로고침될 때마다 생성되게 하였다. 그래서 체크리스트에 변동이 있거나 지도에 마커를 추가/삭제할 때마다 그 즉시 일기가 바뀌도록 하였다.
따라서 Entity, Repositoty, DTO 없이 바로 서비스 로직에서 만들었다.
그런데 아까 말했다시피 무료버전을 사용하기로 한 계획은 망했기 때문에(...) 아래 코드들은 현재 전부 주석처리 해둔 상태다,,,
service -> DiaryService 생성
@Value를 통해 application.properties에 적어두었던 모델 주소와 키를 불러온다.
@Service
public class DiaryService {
@Value("${huggingface.api.url}")
private String apiUrl;
@Value("${huggingface.api.token}")
private String apiToken;
먼저 Http 요청 준비로, 헤더 객체를 생성하여 API 호출에 필요한 정보들을 담는다. JSON 형식으로 지정해야 한다. 본문 객체는 맵으로 생성해서, API의 동작을 조절하는 파라미터를 추가한다. temperature은 높을수록 텍스트의 랜덤성이 증가하고, top_p는 샘플링할 확률 분포를 나타내는 값이다. 이 객체들을 HttpEntity로 하나로 담는다. 이때 requestBody를 JSON으로 직렬화하기 위해 objectMapper를 사용한다.
// API 요청 준비
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(apiToken);
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("inputs", prompt);
requestBody.put("parameters", Map.of(
"max_length", 512,
"temperature", 0.7,
"top_p", 0.9
));
HttpEntity<String> request = new HttpEntity<>(
objectMapper.writeValueAsString(requestBody),
headers
);
restTemplete.exchange 메서드를 사용해서 API를 POST로 호출하고, 결과를 ResponseEntity에 담는다. 응답 상태 코드가 200 ok이고 응답 본문이 존재하는 경우, 일기 텍스트를 반환한다.(생성된 일기 텍스트를 확실히 검증하기 위해 generated_text 키가 포함되어 있는지 확인하였다)
// API 호출 및 응답 처리
ResponseEntity<List<Map<String, String>>> response = restTemplate.exchange(
apiUrl,
HttpMethod.POST,
request,
new ParameterizedTypeReference<List<Map<String, String>>>() {}
);
System.out.println("API response: " + response.getBody()); // 응답 디버깅
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
List<Map<String, String>> responseBody = response.getBody();
System.out.println("API response: " + responseBody); // 디버깅용 로그
if (!responseBody.isEmpty() && responseBody.get(0).containsKey("generated_text")) {
return responseBody.get(0).get("generated_text");
}
}
return "Diary creation failed.";
프롬프트는 아래처럼 작성했다. 처음엔 모르고 한글로 했다가... 내가 보냈던 요청 본문이 응답 본문으로 그대로 되돌아오길래 한참을 고생했다,,,, 그런데 아래처럼 영어로 해도...
private String buildPrompt(ChallengeChecklist checklist) {
//프롬프트 생성
StringBuilder prompt = new StringBuilder();
String dateStr = checklist.getDate().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
prompt.append("I'll tell you what the user did during the day, so please write a message of support along with a brief summary.\n\n");
prompt.append("Today's date is ").append(dateStr);
//체크리스트 정보 추가
if (checklist.isTask1Completed() || checklist.isTask2Completed() || checklist.isTask3Completed()) {
prompt.append("What the user did:\n");
if (checklist.isTask1Completed()) prompt.append("-study: completed\n");
if (checklist.isTask2Completed()) prompt.append("-Walk: completed\n");
if (checklist.isTask3Completed()) prompt.append("-exercise: completed\n");
}
//위치 정보 추가
if (!checklist.getLocations().isEmpty()) {
prompt.append("\nPlaces the user walked:\n");
checklist.getLocations().forEach(location ->
prompt.append(String.format("-Latitude %.6f, Longitude %.6f\n",
location.getLatitude(), location.getLongitude()
))
);
}
prompt.append("\nPlease fill out the form based on the information above.");
return prompt.toString();
}
결과:


랜덤률이 높은가? 싶어서 낮춰봐도


자꾸 헛소리만 한다
파라미터를 최대한 조절해 보면


때려쳐
사실 한 1시간 동안 계속 모델 바꿔가면서 파라미터도 조절해 보고 프롬프트 양식도 바꿔보고 그랬는데...
내가 뭐 잘못한 게 아니라면
결론: 무료로 텍스트 생성까지 바라지 말자
네.
04. 코드 작성 (직접 생성 + 카카오 REST API)
나중에 제대로 해보고 싶으면 그때 유료 버전을 사용해 보기로 하고... 뭐 그래도 위에 거 해보면서 실제 다른 모델이랑 어떻게 Http로 통신해서 답을 받아오는지는 알았으니 공부한 셈 쳤다 하하
암튼 그래서 그냥 내가 직접 작성하기로 함!!
다시 시작 ㄱ
service -> DiraryService 수정
그전 코드들은 전부 주석처리했다.
그리고 직접 할 거니까 application.properties에 적었던 모델 주소나 키도 필요 없다. (혹시 몰라서 그냥 두긴 함)
어차피 체크리스트+지도위치 정보 가지고 일기 생성하는 거니까, 체크리스트만 조건으로 나누면 됐었다. 그래서 모든 경우의 수를 다 적었다... (동숲같이 응원 메시지? 느낌으로 적었더니 완성물은 일기가 아니라 한줄평 같이 되어버렸지만 ㅋㅋ)
public String generateDiary(ChallengeChecklist checklist) {
//프롬프트 생성
StringBuilder prompt = new StringBuilder();
String dateStr = checklist.getDate().format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일"));
prompt.append("날짜: ").append(dateStr).append("\n");
//체크리스트 정보 추가
prompt.append("오늘은 ");
if (checklist.isAllCompleted()) {
prompt.append("공부, 산책, 운동 모두 완료했어! 대단한데?");
} else if (checklist.isTask1Completed() && checklist.isTask2Completed() && !checklist.isTask3Completed()) {
prompt.append("공부, 산책 완료! 열심히 공부하고 밖에도 나갔으니 이 정도면 잘했어~! 내일은 운동도 해보자!!");
}
.
.
.
} else {
prompt.append("잠시 쉬어가는 날이야~ 충전 중...");
}
그리고 위치 정보는 기존에 위도, 경도로 저장되어 있었어서 출력할 때도 일기에 그대로 위도, 경도로 표시되었었다. 이렇게 하면 당연히 사용자(나) 입장에선 이게 대체 어딘지 감도 안 오고 별로 의미도 없어 보였기에... 찾아보니 카카오 맵 api에서 위치를 주소로 변환하는 기능도 제공한다고 한다. 이걸 이용해서 코드를 작성했다.
기존엔 지도 맵 출력+마커관리를 자바스크립트에서 했으니 js키만 발급했었는데, 이번엔 REST API키가 필요하다. 다시 카카오 지도 API 사이트로 가서 해당 키를 복사하고 application.properties에 추가한다.

또한 해당 REST API의 URL은 아래에서 확인할 수 있다. 이것도 같이 복사해서 format에는 json이라고 적고 추가한다.
Kakao Developers
카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.
developers.kakao.com
기존에 js키 추가했던 거 밑에 적어줬다.

위 주소로 queryParam으로 위도, 경도 정보를 보낸다. 난 체크리스트 엔티티에 담겨있는 위치 정보에서 불러왔다.
그리고 아까 실패한() 방식과 비슷하게 HTTP 요청 헤더를 설정하고(여기서는 카카오에서 필요한 정보 담기), restTemplate.exchange로 API를 호출하면서 응답을 저장한다.
요청 코드는 아래 문서에서 지정하는 형식대로 적어준다.

public String getAddress(double latitude, double longitude) {
//요청 URL 생성
String url = UriComponentsBuilder.fromHttpUrl(KAKAO_API_URL)
.queryParam("x", longitude)
.queryParam("y", latitude)
.toUriString();
//HTTP 요청 헤더 설정
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "KakaoAK " + kakaoMapsRestApiKey);
//REST API 호출
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET,
new org.springframework.http.HttpEntity<>(headers), String.class);
// JSON 응답에서 주소 추출
String responseBody = response.getBody();
if (responseBody != null) {
return extractAddressFromJson(responseBody);
}
return "주소를 찾을 수 없습니다.";
}
응답에서 따로 주소 부분만 추출해서 일기 생성로직에 반환하는 형식이다. objectMapper로 JSON을 파싱 하여 첫 번째 문서 서를 추출하고, 거기서 주소 정보를 다시 추출한다. 아래 문서에 따라 순서대로 들어가서, documents -> address -> address_name을 불러오면 된다,


// JSON 응답에서 주소 추출
private static String extractAddressFromJson(String responseBody) {
try {
// Jackson 라이브러리를 사용해 JSON 파싱
com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();
JsonNode rootNode = objectMapper.readTree(responseBody);
JsonNode documents = rootNode.path("documents");
// 첫 번째 문서에서 주소 정보 추출
if (documents.isArray() && !documents.isEmpty()) {
JsonNode addressNode = documents.get(0).path("address");
return addressNode.path("address_name").asText();
}
} catch (Exception e) {
e.printStackTrace();
}
return "주소를 찾을 수 없습니다.";
}
주소 반환이 완료되면, 아까 일기 생성 메서드에서 체크리스트 조건문 다음에 주소 정보가 추가된다. 이것들을 전부 string으로 변환하여 반환하면, 컨트롤러에서 이를 불러오는 형식이다.
//위치 정보 추가
if (!checklist.getLocations().isEmpty()) {
prompt.append("\n\n오늘 갔다왔던 장소들은 여기야!\n");
List<Location> locations = checklist.getLocations();
for (Location location : locations) {
String address = getAddress(location.getLatitude(), location.getLongitude());
prompt.append("-");
prompt.append(address);
prompt.append("\n");
}
} else {
prompt.append("\n");
}
prompt.append("\n하루 정리 끝! 총평: 언제나 별점 만점이야\n");
return prompt.toString();
controller -> ChallengeController 수정
기존에 작성했던 날자 페이지를 GET 하는 부분에서, 일기 생성 로직을 추가로 불러와서 모델에 담아 보낸다. 이때 갑자기 잘되던 날짜 파싱이 오류가 나서... 큰따옴표를 빼는 작업을 추가하였다.
@GetMapping("/date/{date}")
public String datePage(@PathVariable String date, Model model) {
date = date.replace("\"", "");
LocalDate parsedDate = LocalDate.parse(date);
ChallengeChecklist checklist = challengeService.getChallengeByDate(parsedDate);
// Location 정보를 DTO로 변환
List<LocationDTO> locationDTOs = checklist.getLocations().stream()
.map(location -> {
LocationDTO dto = new LocationDTO();
dto.setId(location.getId());
dto.setLatitude(location.getLatitude());
dto.setLongitude(location.getLongitude());
return dto;
})
.collect(Collectors.toList());
//일기 생성
String diary = diaryService.generateDiary(checklist);
model.addAttribute("diary", diary);
model.addAttribute("checklist", checklist);
model.addAttribute("date", date);
model.addAttribute("locations", locationDTOs);
model.addAttribute("kakaoMapsApiKey", kakaoMapsApiKey);
return "date"; // date.html
}
templates -> date.html 수정
기존의 지도 맵, 메인 페이지 되돌아가기 링크 사이에 일기 div를 추가하였다. 컨트롤러에서 보낸 diary 모델을 타임리프로 꺼내면 된다.
<div id="map"></div>
<div class="diary-section">
<h2>오늘의 일기</h2>
<div class="diary-content" th:text="${diary}"></div>
</div>
<a href="/" class="back-link">← 메인 페이지로 이동</a>
05. 실행
체크리스트 경우의 수 모두 다르게 잘 작동되고, 지도 마커 추가/삭제할 때마다 바로 반영되는 것을 볼 수 있다!
사실 이런 주소 말고 정확히 그 위치에 있는 건물명... 맵에 표시된 것처럼 공원 이름이나 마트 이름 등이 나왔었으면 더 좋았겠지만 일단 여기서 만족하기로 했다. 적어도 먼 거리를 마커로 연결했을 때 아래에 바로 몇 개 위치가 있는지 확인할 수 있어서 좋았던 것 같다.
메인 페이지 보완
01. 구상
25.01.15
지도, 일기까지 추가하면서 느낀 건데, 메인 페이지에서 현재 3개 모두 완료했을 때만 녹색으로 뜨는 게 불편했다. 사실상 투두보다는 챌린지가 목적이니 그게 한눈에 알아보기는 쉽겠지만... 문제는 오늘이 며칠인지 헷갈린다는 점이었다. 예를 들어 10일에만 모두 완료해서 녹색으로 완료한 상태고, 11일 12일에는 2개만 해서 기본 흰색이라면, 13일에 접속했을 때 순간 착각하고 빈 색인 11일로 들어가 버렸다...
그리고 2개까지 해도 나름 잘한 것 같은데(내 생각이지만) 아예 안 한 날이랑 같은 취급받는 것도 좀 그래서(...)
기존 3개 완료 녹색에 추가로 1~2개 완료 시엔 노란색으로 뜨게 수정할 것이다.
처음부터 생각했던 거긴 한데 이걸 수정하려면 메인 엔티티에 column을 추가해야 해서 데이터베이스를 수정해야 하는 상황이 벌어져서... 쫄려서 미루고 있다가 이제서야 도전해 본 것이다,,,
02. 코드 작성
domain -> ChallengeChecklist 수정
기존에 작성했던 엔티티에서, 원래는 allCompleted만 있었다. 이제 여기에 부분 완료를 저장할 patiallyCompleted를 추가한다.
@Entity
@Table(name = "challenge_checklist")
@Getter @Setter
public class ChallengeChecklist {
.
.
.
@Column(nullable = false)
private boolean allCompleted = false; //모두 완료: 녹색
@Column(nullable = false)
private boolean partiallyCompleted = false; //1~2개 완료: 노란색
.
.
.
}
application.properties
이번에 자세히 알게 된 점인데, 설정에서 ddl-auto를 update로 해놓으면 위처럼 엔티티를 수정해도 데이터베이스에서 자동으로 수정된다는 점이었다. 다행히 내가 걱정했던 엔티티 오류 문제없이 기존에 저장했던 데이터베이스도 그대로 활용할 수 있었다.
spring.jpa.hibernate.ddl-auto=update
물론 이건 로컬 데이터베이스 기준이고...
이후 완성하고 나서 클라우드타입으로 재배포했는데 반영이 안 돼서 망했다. 찾아보니 클라우드타입에서 무료버전 기준으로 데이터베이스 자동 업데이트 같은 건 지원하지 않는다고 한다. 그래서 그냥 거기서는 기존 마리아db 배포했던 거 삭제하고 다시 배포해서 그전 기록들 직접 다시 저장했다......
사실 이렇게 직접 엔티티를 건드리는 방식 말고도 다른 방식을 생각해 보긴 했었다. 메인 페이지 상단에서 완료 날짜 일 수를 계산해서 진행바를 표시했던 것처럼, 날짜 페이지에서도 비슷하게 완료 개수만 계산하려고 했는데 이건 날짜 페이지마다 다 다르니 서비스로직만으로 해결하기 어려웠다ㅠ.ㅠ(걍 내가 몰라서 못한 걸 수도)
service -> ChallengeService 수정
그래도 엔티티를 수정했으니 서비스는 수정하기 더 간단했다. 그냥 기존 업데이트 로직에서 부분완료 변수도 같이 set해서 레포지토리에 저장해 주면 된다. 이렇게 하면 컨트롤러도 그대로 사용할 수 있고, 메인페이지 화면만 수정하면 된다.
@Transactional
public void updateChecklist(LocalDate date, boolean task1, boolean task2, boolean task3) {
ChallengeChecklist checklist = repository.findByDate(date);
if (checklist != null) {
checklist.setTask1Completed(task1);
checklist.setTask2Completed(task2);
checklist.setTask3Completed(task3);
int completedTasks = (task1 ? 1 : 0) + (task2 ? 1 : 0) + (task3 ? 1 : 0);
checklist.setPartiallyCompleted(completedTasks == 1 || completedTasks == 2);
checklist.setAllCompleted(completedTasks == 3);
repository.save(checklist);
}
}
templates -> main.html 수정
타임리프에서 받아온 challenge 모델에서 변수를 삼항조건으로 판단한다. 이 변수대로 css를 추가하면 된다.
<div class="grid-container">
<a th:each="challenge : ${challenges}"
th:href="@{'/date/' + ${challenge.date}}"
class="date-box"
th:classappend="${challenge.allCompleted} ? 'completed' :
(${challenge.partiallyCompleted} ? 'partially-completed' : '')"
th:text="${#temporals.format(challenge.date, 'MM/dd')}">
</a>
</div>
.completed {
background-color: #90EE90; /* 녹색 */
border-color: #70DD70;
}
.partially-completed {
background-color: #f8f6b4; /* 노란색 */
border-color: #e2dc69;
}
색상 코드는 아래 페이지에서 확인했다. 예전에 vs였나 안드로이드스튜디오에선 코드에서 바로 컬러팔레트로 지정할 수 있었는데... 인텔리제이에선 모르겠어서 그냥 따로 찾았다.
https://wepplication.github.io/tools/colorPicker/
HTML 색상표
온라인 RGB 색상표, HSL, HSV, CSS 칼라코드 모음
wepplication.github.io
03. 실행
이제 3개 완료한 날은 녹색으로, 1~2개 완료한 날은 노란색, 아예 정보가 없는 건 기본 흰색으로 표시된다. 상단에 진행바는 여전히 3개 완료한 녹색일 기준이다.

+
여기까지 하고 클라우드타입에서 새로 작성한 카카오 REST API키도 환경 변수로 설정해 주고, DB도 다시 삭제 후 재배포하면 완료~!!
지금까지 추가한 기능들로도 잘 사용하고 있어서... 아직 더 생각은 안 나지만 나중에 또 필요한 기능이 있으면 추가할 예정,,,

진짜 끝!!!!
'🌟Project > Challenge - 100일 챌린지 웹' 카테고리의 다른 글
| [토이프로젝트 개발일지 03] 산책 경로 저장 기능 추가 (카카오 지도 api) (2) | 2025.01.16 |
|---|---|
| [토이프로젝트 개발일지 02] Spring Boot, MariaDB 배포(CloudType) (1) | 2025.01.12 |
| [토이프로젝트 개발일지 01] 100일 챌린지 웹 (0) | 2025.01.11 |