Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
26ee70a
Merge branch 'develop' into 'master'
JeonJungyu-1 Nov 19, 2023
53cc340
Merge branch 'develop' into 'master'
JeonJungyu-1 Nov 20, 2023
db642c1
Create README.md
JeonJungyu-1 Dec 10, 2023
2af69af
feat: customerror 추가
JungInLee0130 Jun 25, 2024
4d7d68c
feat: plot, notification, comment excetpion -> customexception 교체
JungInLee0130 Jun 25, 2024
1b58f8c
Merge pull request #1 from JungInLee0130/feature/comment
JungInLee0130 Jun 25, 2024
2b47235
chore : plot
JungInLee0130 Jul 16, 2024
0d638f4
Merge pull request #2 from JungInLee0130/feature/junginlee
JungInLee0130 Jul 16, 2024
eca96aa
Update README.md
JungInLee0130 Oct 10, 2024
17a2421
Update README.md
JungInLee0130 Oct 10, 2024
310df8b
update: 리드미 업데이트
JungInLee0130 Dec 6, 2024
8092c7a
Merge branch 'master' of https://github.com/JungInLee0130/novelit int…
JungInLee0130 Dec 6, 2024
71090d0
fix : application.yml 로컬 설정으로 수정
JungInLee0130 Dec 6, 2024
2a1133d
refactor: 댓글 추가,조회,수정 리팩토링 & validation 추가
JungInLee0130 Dec 6, 2024
53476a2
Merge pull request #3 from JungInLee0130/feature/notification
JungInLee0130 Dec 6, 2024
85e5ea5
refactor: notification
JungInLee0130 Dec 6, 2024
f167a1e
test: plotController 플롯 전체 조회 테스트
JungInLee0130 Dec 13, 2024
c0e84b2
refactor: plot 도메인
JungInLee0130 Dec 13, 2024
097b590
Merge pull request #4 from JungInLee0130/feature/notification
JungInLee0130 Dec 14, 2024
0acc0f6
refactor: plot
JungInLee0130 Dec 14, 2024
619f9cb
fix: 키워드를 포함한 전체 플롯 조회 수정
JungInLee0130 Dec 15, 2024
0a2860e
Merge pull request #5 from JungInLee0130/feature/plot
JungInLee0130 Dec 15, 2024
ddaa84a
refactor: 댓글삭제
JungInLee0130 Dec 15, 2024
994c1d1
Merge pull request #6 from JungInLee0130/feature/plot
JungInLee0130 Dec 15, 2024
680ecf1
refactor : plot comment auth 리팩토링
JungInLee0130 Mar 23, 2025
ffbe648
Merge pull request #7 from JungInLee0130/feature/plot
JungInLee0130 Mar 23, 2025
7182778
fix : 키워드 조회 쿼리 수정
JungInLee0130 May 25, 2025
14fafe0
Merge pull request #9 from JungInLee0130/feature/plot
JungInLee0130 May 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
![novelit](https://github.com/galaxy-dan/novelit/assets/85854928/62f15271-bcd8-4938-a419-e0da2c72d6a8)
![novelit2](https://github.com/user-attachments/assets/e99d3f04-a057-4291-9874-fe5488d1c865)

## 주요 기능

### 카카오 로그인

### 작품 관리

1. 작가가 쓴 작품 목록 확인
2. 소설 작품 생성 및 제목 변경, 삭제
3. react-arborist를 사용하여 작품 내의 디렉토리와 파일을 tree 형식으로 보여줌
4. 생성, 삭제, 수정, 이동, 검색 가능

### 글 쓰기(에디터)

1. 글자수 체크
2. 커스텀 버튼을 활용하여 글자 크기, 글꼴, bold 처리 가능
3. 결과물 txt 파일로 다운로드 가능
4. 맞춤법 검사 가능
1. 단어장을 제외한 틀린 맞춤법 검사 결과 제공
5. 단어장 기능
1. 맞춤법 검사에서 제외하도록 함
2. 캐릭터 ,그룹 생성시 단어장에 추가되면 별도로 단어 지정하여 저장 가능
6. 공유
1. 작가가 토큰을 발급하여 편집자에게 넘겨주면 별도의 로그인 없이 해당 글에 대해서만 접속이 가능
2. 특정 부분을 드래그하여 댓글 작성 가능
3. 댓글 달린 부분을 하이라이트 표시
4. 편집자가 댓글을 달면 작가에게 알림
7. 알림
1. 댓글 작성시 댓글 작성자 모두에게 알림이 간다.
- SSE를 활용
- 로그인시 subscribe api 호출을 통해 연결
- 이후 댓글 작성시 서버 → 클라이언트 알림 이벤트 호출
2. 오른쪽 상단의 종모양을 통해 전체 알림을 확인 할 수 있다.
- 알림데이터는 redis에 보관
- 유효기간 1일 설정
- 댓글 작성자 - 작성 내용 : key - value 관계
8. 댓글
1. 본문에 피드백을 남기고 싶은 부분을 하이라이팅하여 댓글 작성 가능
2. MongoDB 활용
- 하이라이팅 부분에 UUID를 부여
- 하이라이팅 공간이 해당 구역에 달린 모든 댓글 작성 정보(작성자, 작성 내용)를 포함하는 nested data형태를 사용함.

### 캐릭터

1. 그룹 페이지에서 그룹 이름 수정 가능
2. 캐릭터 이미지 저장 시 압축
3. 캐릭터 기본정보 다양한 key로 저장 가능
4. 캐릭터 관계
1. 캐릭터 관계에서 캐릭터 목록에 있는 대상 선택 시 글자 검정색, uuid 저장하여 관계도에서 검색 되도록 함,
2. 캐릭터 목록에서 선택하지 않을 시 글자 회색, uuid null로 고치고 관계도에서 검색 안됨

### 관계도

1. cytoscape js 활용
2. 그룹 선택 시 하위 그룹과 캐릭터 확인 가능
3. 캐릭터 선택 시
1. 모든 관계 (그룹포함)
2. 내가 주는 관계
3. 내가 받는 관계
표시
4. 캐릭터와 그룹 이동 시 위치 저장

### 플롯

1. 스토리 흐름을 정리할 수 있는 기능
2. 발단-전개-위기-절정-결말로 나누어 기록
3. 혹은 <줄거리 작성> 부분에 자유롭게 기록
4. 플롯 검색 기능
- QueryDSL 사용
- 키워드를 포함한 플롯의 제목을 검색할때, 다음과 같은 순서로 조회
- 1순위 : 키워드가 제일 앞에 위치
- 2순위 : 키워드를 포함한 제목들

## 기술 스택 및 버전

<aside>
💡 **Server**

</aside>

### `**Back-end server**`

- gradle 8.3
- develop 서버 (**내부포트: docker network, 외부포트 : 8080**)
- prodution 서버 (**내부포트: docker network, 외부포트 : 8080**)
- spring boot - 3.1.5 ver.
- jdk - 17 ver.
- querydsl - querydsl-jap:5.0.0 ver.
- JWT - io.jsonwebtoken:jjwt 0.9.1 ver.
- Spring Security - spring boot 버전과 동일
- JPA - spring boot 버전과 동일

### `**DataBase server**`

- MariaDB - 11.0.2 ver.
- MariaDB - **포트 : 3306**
- MongoDB - (v 7.0.2)
- redis version:7.2.3 - 포트 : 3503 → 6379 (aws포트 - 컨테이너포트)

### **`Infra`**

- AWS EC2
- Docker - v.24.0.6
- Jenkins - 포트: 3000
- Nginx - v.1.18.0
- nGrinder - 포트: 3800
- prometheus - 포트: 3500 → 9090
- node-exporter - 포트: 3501 → 9091
- grafana - 포트: 3502 → 3000

<aside>
💡 **Web Frontend**

</aside>

- Next.js 13.4.12
- typescript 5.1.6
- recoil 0.7.7
- tailwind 3.3.3
- tanstack/react-query 4.32.0
- react-hook-form 7.45.2
- framer-motion 10.16.4
- react-icons 4.10.1
- react-intersection-observer 9.5.2
- react-aws-s3 1.5.0
- react-spinners 0.13.8
- yup 1.2.0

<aside>
💡 **Cooperation**

</aside>

- JIRA
- Notion
- Slack
- Git Lab
8 changes: 8 additions & 0 deletions server/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
###
### 설정파일 ###
src/main/resources/data.sql
src/main/resources/env_local.yml
src/main/resources/env_aws.yml
src/main/resources/env_monitoring.yml
src/main/resources/env_test.yml
docker-compose/.env
8 changes: 7 additions & 1 deletion server/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,14 @@ out/
### VS Code ###
.vscode/

### data.sql ###
src/main/resources/data.sql

### 설정파일 ###
src/main/resources/env.yml
src/main/resources/env_local.yml
src/main/resources/env_aws.yml
src/main/resources/env_monitoring.yml
src/main/resources/env_test.yml
docker-compose/.env

# Created by https://www.toptal.com/developers/gitignore/api/macos
Expand Down
9 changes: 3 additions & 6 deletions server/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
FROM openjdk:17
ARG IDLE_PROFILE
ARG JAR_FILE=*.jar
ENV ENV_IDLE_PROFILE=$IDLE_PROFILE
FROM openjdk:17-jdk
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
RUN echo $ENV_IDLE_PROFILE
ENTRYPOINT ["java", "-Dspring.profiles.active=${ENV_IDLE_PROFILE}", "-jar","/app.jar"]
ENTRYPOINT ["java", "-Dspring.profiles.active=docker", "-jar", "app.jar"]
19 changes: 13 additions & 6 deletions server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,11 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation "org.springframework.boot:spring-boot-starter-actuator"

implementation "org.springframework.kafka:spring-kafka"
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'

// implementation 'org.springdoc:springdoc-openapi-ui:1.7.0'
Expand All @@ -45,15 +44,12 @@ dependencies {
developmentOnly 'org.springframework.boot:spring-boot-devtools'
// developmentOnly 'org.springframework.boot:spring-boot-docker-compose'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// testImplementation 'org.springframework.security:spring-security-test'


//mapStruct
implementation 'org.mapstruct:mapstruct:1.5.3.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'

// implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
// implementation "com.querydsl:querydsl-apt:${queryDslVersion}"
//Querydsl 추가
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
Expand All @@ -70,6 +66,14 @@ dependencies {

// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// string boot actuator, prometheus
implementation "org.springframework.boot:spring-boot-starter-actuator"
implementation "io.micrometer:micrometer-registry-prometheus"

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'com.h2database:h2'
}

tasks.named('test') {
Expand Down Expand Up @@ -126,3 +130,6 @@ configurations {
// querydsl.extendsFrom compileClasspath
//}

jar {
enabled = false
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.kafka.config.TopicBuilder;
import org.springframework.kafka.core.KafkaAdmin.NewTopics;

@SpringBootApplication
public class NovelitApplication {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public class AuthServiceImpl implements AuthService{
private String KAKAO_CLIENT_SECRET;

@Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}")
private String KAKAO_REDIRECT_URL;
private String KAKAO_REDIRECT_URI;

@Value("${spring.security.oauth2.client.provider.kakao.token-uri}")
private String KAKAO_TOKEN_URL;
Expand All @@ -57,7 +57,9 @@ public class AuthServiceImpl implements AuthService{
private final RedisTemplate<String, String> redisTemplate;
@Override
public LoginResDTO kakaoLogin(String code) {
// 카카오 엑세스 토큰을 요청하는 함수
KaKaoAccessTokenDTO kakaoAccessToken = getAccessToken(code);
// 카카오 유저정보를 요청하는 함수
KakaoUserInfoDTO kakaoUserInfo = getKakaoUserInfo(kakaoAccessToken);
String email = (String)kakaoUserInfo.getKakaoAccount().get("email");
String nickname = kakaoUserInfo.getProperties().get("nickname");
Expand All @@ -77,7 +79,9 @@ public LoginResDTO kakaoLogin(String code) {
.build());
}

// 유저정보를 저장하고 Authentication을 발급받음.
Authentication authenticate = new UsernamePasswordAuthenticationToken(userUUID, "", List.of(() -> "USER"));
// SecurityContextHolder에 인증정보를 저장하고, accessToken, RefreshToken을 생성하여 redis에 저장
SecurityContextHolder.getContext().setAuthentication(authenticate);
String accessToken = jwtUtils.generateAccessToken(authenticate);
String refreshToken = jwtUtils.generateRefreshToken(authenticate);
Expand All @@ -89,6 +93,7 @@ public LoginResDTO kakaoLogin(String code) {
TimeUnit.MILLISECONDS
);

// API로 요청한 accessToken, RefreshToken 정보를 리턴
return new LoginResDTO(accessToken, refreshToken);
}

Expand All @@ -98,7 +103,7 @@ private KaKaoAccessTokenDTO getAccessToken(String code) {
params.add("grant_type", "authorization_code");
params.add("client_id", KAKAO_CLIENT_ID);
params.add("client_secret", KAKAO_CLIENT_SECRET);
params.add("redirect_uri", KAKAO_REDIRECT_URL);
params.add("redirect_uri", KAKAO_REDIRECT_URI);
params.add("code", code);


Expand All @@ -114,10 +119,13 @@ private KaKaoAccessTokenDTO getAccessToken(String code) {

private KakaoUserInfoDTO getKakaoUserInfo(KaKaoAccessTokenDTO kakaoAccessToken) {
HttpHeaders headers = new HttpHeaders();

// 카카오에서 발급받은 엑세스토큰에 Authorization, Bearer을 추가해 해더에 셋팅.
headers.set("Authorization", "Bearer " + kakaoAccessToken.getAccessToken());

HttpEntity<Object> entity = new HttpEntity<>(headers);

// 그대로 restTemplate를 사용해서 KAKAO_INFO_URL로 요청 때림. 카카오 유저정보를 받아옴.
return restTemplate.postForObject(KAKAO_INFO_URL, entity, KakaoUserInfoDTO.class);
}

Expand Down
17 changes: 15 additions & 2 deletions server/src/main/java/com/galaxy/novelit/author/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.UUID;

@Entity
@Table(name = "user")
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class User {
@Id
@Column(name = "user_id")
Expand All @@ -32,4 +32,17 @@ public class User {
@Column(name = "nickname", length = 16, nullable = false)
private String nickname;

@Builder
public User(String userUUID, String email, String nickname) {
this.userUUID = userUUID;
this.email = email;
this.nickname = nickname;
}

@Builder
public User(String email, String nickname) {
this.userUUID = String.valueOf(UUID.randomUUID());
this.email = email;
this.nickname = nickname;
}
}
Loading