프로젝트
[ bible cash ] 각 계층별 코드
do_it_zero
2025. 1. 17. 18:00
config
package study.demobible.config;
import jakarta.servlet.Filter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import study.demobible.config.filter.SessionFilter;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<SessionFilter> sessionFilter(){
FilterRegistrationBean<SessionFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new SessionFilter());
registrationBean.addUrlPatterns("/home","/history","/contents");
return registrationBean;
}
}
package study.demobible.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableScheduling
public class SchedulingConfig {
}
package study.demobible.config.filter;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import study.demobible.util.MemberContext;
import java.io.IOException;
public class SessionFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 만약 만료되었으면 session은 null
HttpSession session = httpRequest.getSession(false);
if(session == null){
httpResponse.sendRedirect(httpRequest.getContextPath() + "/");
return;
}
long lastAccessedTime = session.getLastAccessedTime();
long currentTime = System.currentTimeMillis();
// 세션 생성시간과 현재 시간을 비교해서 timeout보다 클 경우 세션을 만료시킨다.
long timeout = 10000;
if(currentTime - lastAccessedTime > timeout){
session.invalidate();
httpResponse.sendRedirect(httpRequest.getContextPath() + "/");
return;
}
chain.doFilter(request,response);
}
}
controller
package study.demobible.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import study.demobible.dto.ContentDto;
import study.demobible.entity.Content;
import study.demobible.service.ContentService;
import java.time.LocalDate;
import java.util.List;
@Slf4j
@Controller
@RequiredArgsConstructor
public class ContentController {
private final ContentService contentService;
@GetMapping("/contents")
public String getContents(@RequestParam("year") int year,
@RequestParam("month") int month,
@RequestParam("date") int date,
Model model){
log.info("Date: {}-{}-{}", year, month, date);
/**
* 1. 해당 날짜에 맞는 content를 가져온다.
* 2. ReadingHistory에서 readingStatus를 가져온다.
* 3. model에 담아서 전송한다.
* */
LocalDate newDate = LocalDate.of(year, month, date);
List<ContentDto> contentDtoList = contentService.getContent(newDate);
//log.info("contentList: {}",contentDtoList);
model.addAttribute("contentDtoList",contentDtoList);
model.addAttribute("newDate",newDate);
//contentService.getReadingStatus();
return "content";
}
}
package study.demobible.controller;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import study.demobible.entity.History;
import study.demobible.entity.Member;
import study.demobible.entity.ReadingStatus;
import study.demobible.repository.HistoryRepository;
import study.demobible.service.HistoryService;
import study.demobible.util.MemberContext;
import java.time.LocalDate;
import java.time.LocalDateTime;
import static study.demobible.entity.ReadingStatus.*;
@Controller
@Slf4j
@RequiredArgsConstructor
public class HistoryController {
private final HistoryService historyService;
@GetMapping("/history")
public String saveHistory(HttpSession session){
Member member = MemberContext.getMember(session);
historyService.saveHistory(member);
return "home";
}
}
package study.demobible.controller;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import study.demobible.entity.History;
import study.demobible.entity.Member;
import study.demobible.repository.HistoryRepository;
import study.demobible.repository.TalentRepository;
import java.util.List;
@Controller
@RequiredArgsConstructor
public class HomeController {
private final HistoryRepository historyRepository;
private final TalentRepository talentRepository;
@GetMapping("/home")
public String home(HttpSession session, Model model){
// 읽음 이력 가져오기
Member member =(Member) session.getAttribute("member");
List<History> historyList = historyRepository.findByMemberIdx(member.getIdx());
// member 총합 talent 가져오기
return "home";
}
}
package study.demobible.controller;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.Banner;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import study.demobible.dto.LoginForm;
import study.demobible.dto.SignUpForm;
import study.demobible.entity.Member;
import study.demobible.repository.MemberRepository;
import study.demobible.util.MemberContext;
@Slf4j
@Controller
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
@GetMapping("/signup")
public String getSignupPage(){
return "signup";
}
@PostMapping("/signup")
public String signup(@ModelAttribute SignUpForm signUpForm, Model model,HttpSession session){
Member member = new Member(signUpForm);
Member savedMember = memberRepository.save(member);
// 세션에 멤버 저장 서버에서만 확인 가능 세션은 클라이언트마다 각각임.
session.setAttribute("member",savedMember);
model.addAttribute("id",signUpForm.getName());
return "home";
}
@PostMapping("/login")
public String login(@ModelAttribute LoginForm loginForm, Model model, HttpSession session){
log.info("loginForm : {}",loginForm);
Member findMember = memberRepository.findByUserId(loginForm.getUserId());
session.setAttribute("member",findMember);
model.addAttribute("id",findMember.getUserId());
return "home";
}
}
dto
package study.demobible.dto;
import lombok.Data;
import study.demobible.entity.Content;
@Data
public class ContentDto {
private Long idx;
private String longLabel;
private Integer chapter;
private Integer paragraph;
private String sentence;
public ContentDto(Content content){
this.idx = content.getIdx();
this.longLabel = content.getLongLabel();
this.chapter = content.getChapter();
this.paragraph = content.getParagraph();
this.sentence = content.getSentence();
}
}
package study.demobible.dto;
import lombok.Data;
@Data
public class LoginForm {
private String userId;
}
package study.demobible.dto;
import lombok.Data;
import lombok.ToString;
import study.demobible.entity.Group;
@Data
@ToString
public class SignUpForm {
private String userId;
private String name;
private Group group;
}
entity
package study.demobible.entity;
import jakarta.persistence.*;
import lombok.Getter;
@Entity
@Getter
public class Content {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long idx;
@Column(nullable = false)
private Integer cate;
@Column(nullable = false)
private Integer book;
@Column(nullable = false)
private Integer chapter;
@Column(nullable = false)
private Integer paragraph;
@Column(nullable = false, columnDefinition = "TEXT")
private String sentence;
@Column(nullable = false, length = 10)
private String testament;
@Column(nullable = false, length = 30)
private String longLabel;
@Column(nullable = false, length = 10)
private String shortLabel;
}
package study.demobible.entity;
public enum Group {
A,B,C
}
package study.demobible.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.random.RandomGenerator;
@Entity
@NoArgsConstructor
@Getter
public class History {
@Id
@GeneratedValue
private Long idx;
@Column(columnDefinition = "DATE")
private LocalDate readTime;
private ReadingStatus readingStatus;
@ManyToOne
private Member member;
@ManyToOne
private Range range;
public History(LocalDate readTime, ReadingStatus readingStatus, Member member,Range range) {
this.readTime = readTime;
this.readingStatus = readingStatus;
this.member = member;
this.range = range;
}
}
package study.demobible.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import study.demobible.dto.SignUpForm;
import study.demobible.repository.HistoryRepository;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@ToString
@NoArgsConstructor
public class Member {
@Id
@GeneratedValue
private Long idx;
@Column(unique = true)
private String userId;
private String name;
@Enumerated(EnumType.STRING)
@Column(name = "member_group")
private Group group;
@OneToMany(mappedBy = "member")
private List<History> historyList = new ArrayList<>();
public Member(String userId, String name, Group group) {
this.userId = userId;
this.name = name;
this.group = group;
}
public Member(SignUpForm signUpForm){
this.userId = signUpForm.getUserId();
this.name = signUpForm.getName();
this.group = signUpForm.getGroup();
}
}
package study.demobible.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Entity
@Getter
@ToString
@NoArgsConstructor
public class Range {
@Id
@GeneratedValue
private Long idx;
private int startPoint;
private int endPoint;
@Column(columnDefinition = "DATE")
private LocalDate updatedAt;
public Range(int startPoint, int endPoint, LocalDate updatedAt) {
this.startPoint = startPoint;
this.endPoint = endPoint;
this.updatedAt = updatedAt;
}
}
package study.demobible.entity;
public enum ReadingStatus {
READ,NOT_READ
}
package study.demobible.entity;
import jakarta.persistence.*;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
@Entity
@NoArgsConstructor
public class Talent {
@Id
@GeneratedValue
private Long idx;
@ManyToOne
private Member member;
@Column(columnDefinition = "int default 0")
private int talent;
@Column(columnDefinition = "DATE")
private LocalDate updatedAt;
public Talent(Member member,LocalDate rangeUpdatedAt){
this.member = member;
this.updatedAt = updatedAt;
if(rangeUpdatedAt.isEqual(LocalDate.now())){
this.talent = 10;
} else {
this.talent = 2;
}
}
}
package study.demobible.entity;
import java.time.LocalDateTime;
public class TalentHistory {
private Long idx;
private Talent talent;
private LocalDateTime updatedAt;
private int updatedTalent;
}
exception
package study.demobible.exception;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class ClassExceptionHandlerAspect {
@Pointcut("within(@HandleRuntimeException *)")
public void classAnnotatedWithHandleException(){}
@Around("classAnnotatedWithHandleException()")
public Object handleRuntimeException(ProceedingJoinPoint joinPoint){
try {
return joinPoint.proceed();
} catch (Throwable throwable){
if(throwable instanceof RuntimeException){
HandleRuntimeException handleRuntimeException = joinPoint.getTarget().getClass().getAnnotation(HandleRuntimeException.class);
for(Class<? extends RuntimeException> exceptionClass : handleRuntimeException.value()){
if(exceptionClass.isInstance(throwable)){
log.info("Handle exception: {}",throwable.getClass().getSimpleName());
log.info("Message: {}",throwable.getMessage());
return null; // 예외 처리 후 기본 반환 값
}
}
}
throw new RuntimeException(throwable);
}
}
}
package study.demobible.exception;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface HandleRuntimeException {
Class<? extends RuntimeException>[] value() default {RuntimeException.class};
}
repository
package study.demobible.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import study.demobible.entity.Content;
import java.util.List;
@Repository
public interface ContentRepository extends JpaRepository<Content,Long> {
List<Content> findByIdxBetween(int startIdx, int endIdx);
}
package study.demobible.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import study.demobible.entity.History;
import java.util.List;
public interface HistoryRepository extends JpaRepository<History,Long> {
List<History> findByMemberIdx(Long memberIdx);
}
package study.demobible.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import study.demobible.entity.Member;
public interface MemberRepository extends JpaRepository<Member,Long> {
Member findByUserId(String userId);
}
package study.demobible.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import study.demobible.entity.Range;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Optional;
public interface RangeRepository extends JpaRepository<Range,Long> {
Range findTopByOrderByUpdatedAtDesc();
//Optional<Range> findByUpdatedAtBetween(LocalDateTime startOfDay, LocalDateTime endOfDay);
Optional<Range> findByUpdatedAt(LocalDate dateTime);
}
package study.demobible.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import study.demobible.entity.Talent;
public interface TalentRepository extends JpaRepository<Talent,Long> {
}
service
package study.demobible.service.schedul;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import study.demobible.entity.Range;
import study.demobible.repository.RangeRepository;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Slf4j
@Service
@RequiredArgsConstructor
public class RangeScheduler {
private final RangeRepository rangeRepository;
@Scheduled(cron = "0 0 0 * * *") // 매일 자정 실행됨
//@Scheduled(fixedRate = 30000) // 30초마다 실행
public void updateRange(){
Range currentRange = rangeRepository.findTopByOrderByUpdatedAtDesc();
if (currentRange != null) {
int newStart;
int newEnd;
// end 값이 31100인 경우
if (currentRange.getEndPoint() == 31100) {
newStart = currentRange.getEndPoint() + 1;
newEnd = currentRange.getEndPoint() + 38;
} else {
// 그 외의 경우
newStart = currentRange.getEndPoint() + 1;
newEnd = newStart + 99;
}
// 계산된 값으로 Range 객체 생성 및 저장
Range range = new Range(newStart, newEnd, LocalDate.now());
rangeRepository.save(range);
log.info("업데이트 된 range: start = {} end = {}", newStart, newEnd);
} else {
// 초기 데이터 생성
Range initRange = new Range(1, 100, LocalDate.now());
rangeRepository.save(initRange);
log.info("초기 데이터 생성: start = 1, end = 100");
}
}
}
package study.demobible.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import study.demobible.dto.ContentDto;
import study.demobible.entity.Content;
import study.demobible.entity.Range;
import study.demobible.exception.HandleRuntimeException;
import study.demobible.repository.ContentRepository;
import study.demobible.repository.RangeRepository;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Slf4j
@Service
@HandleRuntimeException
@RequiredArgsConstructor
public class ContentService {
private final ContentRepository contentRepository;
private final RangeRepository rangeRepository;
public List<ContentDto> getContent(LocalDate localDate) {
// 외부 날짜를 LocalDateTime로 변환함.
//LocalDateTime localDateTime = LocalDateTime.of(year,month,date,0,0);
//LocalDateTime startOfDay = localDate.atStartOfDay();
//LocalDateTime endOfDay = localDate.atTime(23, 59, 59, 999999);
// 해당 날짜에 해당하는 Range를 가져온다.
//Optional<Range> range = rangeRepository.findByUpdatedAtBetween(startOfDay,endOfDay);
Optional<Range> range = rangeRepository.findByUpdatedAt(localDate);
log.info("rage: {}",range);
// Range에 해당하는 Content들을 가져온 후 ContentDto로 변환
List<Content> contentList = contentRepository.findByIdxBetween(range.get().getStartPoint(), range.get().getEndPoint());
List<ContentDto> contentDtoList = contentList.stream()
.map(ContentDto::new)
.sorted(Comparator.comparing(ContentDto::getIdx))
.collect(Collectors.toList());
return contentDtoList;
}
}
package study.demobible.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import study.demobible.entity.*;
import study.demobible.exception.HandleRuntimeException;
import study.demobible.repository.HistoryRepository;
import study.demobible.repository.RangeRepository;
import study.demobible.repository.TalentRepository;
import study.demobible.util.MemberContext;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Optional;
import static study.demobible.entity.ReadingStatus.*;
@Slf4j
@Service
@Transactional
@HandleRuntimeException
@RequiredArgsConstructor
public class HistoryService {
private final HistoryRepository historyRepository;
private final RangeRepository rangeRepository;
private final TalentRepository talentRepository;
public void saveHistory(Member member){
LocalDate readTime = LocalDate.now();
Optional<Range> optionalRange = rangeRepository.findByUpdatedAt(readTime);
Range range = optionalRange.orElseThrow(() -> new IllegalStateException("해당 Range는 존재하지 않습니다."));
History history = new History(readTime, READ, member, range);
History savedHistory = historyRepository.save(history);
log.info("저장된 기록 : {}",savedHistory);
Talent talent = new Talent(member, readTime);
Talent savedTalent = talentRepository.save(talent);
log.info("저장된 달란트 : {}",savedTalent);
}
}
package study.demobible.service;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import study.demobible.dto.SignUpForm;
import study.demobible.entity.Member;
import study.demobible.repository.MemberRepository;
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
public void save(SignUpForm signUpForm){
Member member = new Member(signUpForm);
memberRepository.save(member);
}
}
css
/* content style */
body {
margin: 0;
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: flex-start; /* 왼쪽 정렬로 변경 */
justify-content: flex-start; /* 왼쪽 상단에 정렬 */
background-color: #f4f4f4;
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 10px;
}
.current-month {
font-size: 16px;
font-weight: bold;
text-align: center;
flex-grow: 1;
}
.nav-button {
padding: 5px 15px;
font-size: 14px;
color: #fff;
background-color: #007bff;
border: none;
border-radius: 5px;
cursor: pointer;
margin: 0 5px;
transition: background-color 0.3s ease;
}
.nav-button:hover {
background-color: #0056b3;
}
/* 하단 버튼 스타일 */
.footer {
text-align: center;
margin-top: 20px; /* 버튼과 이미지, 텍스트 간의 간격 */
}
/* 기본 스타일 */
body {
margin: 0;
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #f4f4f4;
}
.container {
max-width: 100%; /* 반응형 크기 */
padding: 10px;
box-sizing: border-box;
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 10px;
}
.current-month {
font-size: 16px;
font-weight: bold;
text-align: center;
flex-grow: 1;
}
.nav-button {
padding: 5px 15px;
font-size: 14px;
color: #fff;
background-color: #007bff;
border: none;
border-radius: 5px;
cursor: pointer;
margin: 0 5px;
transition: background-color 0.3s ease;
}
.nav-button:hover {
background-color: #0056b3;
}
#daysOfWeek {
display: grid;
grid-template-columns: repeat(7, 1fr);
text-align: center;
font-size: 14px;
font-weight: bold;
margin-bottom: 5px;
}
.day-name {
padding: 5px;
color: #555;
}
#calendar {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 5px;
padding: 10px;
background: #ffffff;
border-radius: 10px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.calendar-day {
text-align: center;
padding: 10px;
border-radius: 5px;
font-size: 14px;
position: relative;
background-color: #fafafa;
cursor: pointer;
transition: background-color 0.3s ease;
}
.calendar-day:hover {
background-color: #e6f7ff;
}
.calendar-day.disabled {
color: #ccc;
pointer-events: none;
}
.calendar-day.today {
background-color: #ffe6e6;
}
.calendar-day.selected {
background-color: #b3d9ff;
}
.calendar-day.today .dot {
width: 5px;
height: 5px;
background-color: red;
border-radius: 50%;
position: absolute;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
}
/* 하단 버튼 스타일 */
.footer {
text-align: center;
margin-top: 20px; /* 버튼과 이미지, 텍스트 간의 간격 */
}
.verse-button {
padding: 10px 20px;
font-size: 16px;
color: #fff;
background-color: #28a745;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.verse-button:hover {
background-color: #218838;
}
/* 동전 이미지 및 달란트 스타일 */
.coin-info {
display: flex;
justify-content: center;
align-items: center;
margin-top: 10px; /* '오늘의 말씀' 버튼과 동전 이미지 사이 간격 */
}
.coin-info img {
width: 1em; /* 동전 이미지 크기를 텍스트 크기에 맞게 설정 */
height: 1em;
margin-right: 5px; /* 텍스트와 이미지 사이 간격 */
}
.coin-text, .coin-amount {
font-size: 1em;
}
.left-aligned {
text-align: left;
margin-left: 0;
margin-bottom: 10px;
}
/* 반응형 디자인 */
@media (max-width: 600px) {
#calendar {
gap: 3px;
}
.calendar-day {
padding: 8px;
font-size: 12px;
}
.nav-button {
padding: 4px 10px;
font-size: 12px;
}
.current-month {
font-size: 14px;
}
.verse-button {
padding: 8px 15px;
font-size: 14px;
}
.dropdown {
padding: 5px 10px;
font-size: 14px;
border-radius: 5px;
border: 1px solid #ccc;
margin-right: 10px;
}
.year-text {
font-size: 16px;
font-weight: bold;
margin-right: 10px;
}
}
js
document.addEventListener('DOMContentLoaded', function () {
const calendarEl = document.getElementById('calendar');
const daysOfWeekEl = document.getElementById('daysOfWeek');
const monthSelect = document.getElementById('monthSelect');
const todayButton = document.getElementById('todayButton');
const yearText = document.getElementById('yearText');
const verseButton = document.getElementById('verseButton');
const today = new Date();
let currentYear = 2025; // 고정된 연도 2025
let currentMonth = today.getMonth();
let selectedDate = today; // 기본적으로 오늘 날짜 선택
const daysOfWeek = ['일', '월', '화', '수', '목', '금', '토'];
// 초기화
populateMonthSelect();
updateSelectValues();
renderCalendar(currentYear, currentMonth);
// 이벤트 리스너
monthSelect.addEventListener('change', function () {
currentMonth = parseInt(monthSelect.value, 10);
renderCalendar(currentYear, currentMonth);
});
todayButton.addEventListener('click', function () {
currentMonth = today.getMonth();
selectedDate = today; // 오늘 날짜로 설정
renderCalendar(currentYear, currentMonth);
updateVerseButton('오늘의 말씀');
});
// "오늘의 말씀" 버튼 클릭 시 선택된 날짜 전송
verseButton.addEventListener('click', function () {
if (selectedDate) {
const year = selectedDate.getFullYear();
const month = selectedDate.getMonth() + 1; // 월은 0부터 시작하므로 +1
const date = selectedDate.getDate();
// 서버에 선택된 날짜 전송 (서버로 이동)
const url = `/contents?year=${year}&month=${month}&date=${date}`;
// 페이지 이동 (서버로 요청을 보내는 방식)
window.location.href = url;
} else {
alert("먼저 날짜를 선택해주세요.");
}
});
function populateMonthSelect() {
for (let month = 0; month < 12; month++) {
const option = document.createElement('option');
option.value = month;
option.textContent = `${month + 1}월`;
monthSelect.appendChild(option);
}
}
function updateSelectValues() {
monthSelect.value = currentMonth;
}
function renderCalendar(year, month) {
calendarEl.innerHTML = ''; // 기존 달력 초기화
daysOfWeekEl.innerHTML = ''; // 요일 초기화
// 요일 렌더링
daysOfWeek.forEach((day) => {
const dayEl = document.createElement('div');
dayEl.className = 'day-name';
dayEl.textContent = day;
daysOfWeekEl.appendChild(dayEl);
});
// 날짜 계산
const daysInMonth = new Date(year, month + 1, 0).getDate();
const firstDay = new Date(year, month, 1).getDay();
// 빈 셀 추가
for (let i = 0; i < firstDay; i++) {
const emptyDay = document.createElement('div');
emptyDay.className = 'calendar-day empty';
calendarEl.appendChild(emptyDay);
}
// 날짜 생성
for (let day = 1; day <= daysInMonth; day++) {
const dayEl = document.createElement('div');
dayEl.className = 'calendar-day';
const date = new Date(year, month, day);
// 오늘 날짜는 빨간 점 표시
if (
date.getFullYear() === today.getFullYear() &&
date.getMonth() === today.getMonth() &&
date.getDate() === today.getDate()
) {
dayEl.classList.add('today');
const dot = document.createElement('div');
dot.className = 'dot';
dayEl.appendChild(dot);
}
// 선택한 날짜는 다른 스타일 적용
if (
selectedDate &&
date.getFullYear() === selectedDate.getFullYear() &&
date.getMonth() === selectedDate.getMonth() &&
date.getDate() === selectedDate.getDate()
) {
dayEl.classList.add('selected');
}
// 오늘 이후 날짜는 선택 불가
if (date > today) {
dayEl.classList.add('disabled');
dayEl.style.pointerEvents = 'none';
}
// 날짜 클릭 시 오늘의 말씀으로 변경
dayEl.textContent = day;
dayEl.addEventListener('click', function () {
if (date <= today) { // 오늘 이전 날짜는 클릭 시 말씀이 표시
selectedDate = date;
renderCalendar(currentYear, currentMonth);
if (date.getFullYear() === today.getFullYear() &&
date.getMonth() === today.getMonth() &&
date.getDate() === today.getDate()) {
updateVerseButton('오늘의 말씀');
} else {
updateVerseButton(`${date.getFullYear()}년 ${date.getMonth() + 1}월 ${date.getDate()}일의 말씀`);
}
}
});
calendarEl.appendChild(dayEl);
}
}
function updateVerseButton(text) {
verseButton.textContent = text;
}
window.showVerse = function () {
alert('오늘의 말씀: "나는 길이요 진리요 생명이다" (요한복음 14:6)');
};
});
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>성경 구절</title>
<link rel="stylesheet" th:href="@{/css/contentStyle.css}">
<!-- 부트스트랩 CSS CDN -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<!-- 부트스트랩 아이콘 (선택 사항) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css">
</head>
<body>
<div th:each="contentDto, iterStat : ${contentDtoList}">
<div th:if="${iterStat.index == 0 or contentDto.getParagraph() == 1}">
<h2 th:text="${contentDto.getLongLabel()} + ' ' + ${contentDto.getChapter()} + '장'"></h2>
</div>
<div th:text="${contentDto.getParagraph()} + '. ' + ${contentDto.getSentence()}" style="margin-bottom: 10px;"></div>
</div>
<div class="d-flex justify-content-between mt-3">
<form action="/history" method="POST">
<input type="hidden" name="newDate" value="${newDate}">
<button type="submit" class="btn-secondary btn-block text-center">읽기 완료</button>
</form>
</div>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>반응형 달력</title>
<link rel="stylesheet" th:href="@{/css/style.css}">
</head>
<body>
<div class="container">
<div class="calendar-header">
<div class="current-month" id="yearText">2025년</div>
<select id="monthSelect" class="dropdown"></select>
<button class="nav-button" id="todayButton">오늘</button>
</div>
<div id="daysOfWeek"></div>
<div id="calendar"></div>
<div class="footer">
<!-- '오늘의 말씀' 버튼을 link로 감싸지 않고, 클릭 이벤트로 처리하도록 수정 -->
<button class="verse-button" id="verseButton">오늘의 말씀</button>
<div class="coin-info">
<img th:src="@{/images/coin.png}" alt="동전 이미지"> <!-- 동전 이미지 파일 경로 -->
<span class="coin-text">달란트</span>
<span class="coin-amount">: 100</span>
</div>
</div>
</div>
<!-- JavaScript -->
<script th:src="@{/js/calendar.js}"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>로그인 페이지</title>
<!-- 부트스트랩 CSS CDN -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<!-- 부트스트랩 아이콘 (선택 사항) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css">
</head>
<body class="d-flex justify-content-center align-items-center" style="height: 100vh; background-color: #f4f6f9;">
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<!-- 중앙 박스 -->
<div class="card shadow">
<div class="card-body p-4">
<!-- 제목 -->
<h3 class="text-center mb-4">테스트 중</h3>
<form th:action="@{/login}" method="post">
<!-- 아이디 입력란 -->
<div class="form-group">
<label for="userId">아이디</label>
<input type="text" id="userId" name="userId" class="form-control" placeholder="아이디를 입력하세요" required>
</div>
<button type="submit" class="btn btn-primary btn-block">로그인</button>
</form>
<div class="d-flex justify-content-between mt-3">
<!-- 회원가입 버튼 -->
<a href="/signup" class="btn-secondary btn-block text-center">회원가입</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 부트스트랩 JS 및 Popper.js CDN -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.2/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>회원가입</title>
<!-- 부트스트랩 CSS CDN -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
</head>
<body>
<div class="container mt-5">
<h2 class="text-center">여정 합류</h2>
<!-- 회원가입 폼 -->
<form action="/signup" method="POST">
<div class="form-group">
<label for="group">아이디 그룹</label>
<!-- 드롭다운 선택 -->
<select id="group" name="group" class="form-control">
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
</select>
</div>
<div class="form-group">
<label for="userId">유저id</label>
<input type="text" id="userId" class="form-control" name="userId" placeholder="아이디를 입력하세요">
</div>
<div class="form-group">
<label for="name">이름</label>
<input type="text" id="name" class="form-control" name="name" placeholder="아이디를 입력하세요">
</div>
<button type="submit" class="btn btn-primary btn-block">회원가입</button>
</form>
</div>
</body>
</html>
application.yml
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/bible
username: sa
password: sa
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
logging:
level:
org.hibernate.sql: debug