diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6c41d33
--- /dev/null
+++ b/README.md
@@ -0,0 +1,117 @@
+# SejongAuth
+
+SejongAuth는 세종대학교 통합 로그인 페이지에 접근하여 사용자 인증과 프로필 정보를 가져오는 Java 기반의 패키지입니다. `Sj`를 통해 세종대학교 포털에 간편하게 로그인하고 사용자의 학적 정보를 조회할 수 있습니다.
+
+## 주요 기능
+
+- **로그인 기능**: `LoginReq`를 사용하여 세종대학교 포털에 로그인하고 `JSESSIONID`를 획득합니다.
+- **프로필 조회 기능**: 로그인 성공 시 `ProfileService`를 통해 사용자의 학적 정보를 가져옵니다.
+
+---
+
+## 설치 방법
+
+gradle
+```properties
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ mavenCentral()
+ maven { url 'https://jitpack.io' }
+ }
+ }
+dependencies {
+ implementation 'com.github.urinaner:sejong-auth:Tag'
+ }
+```
+maven
+```yaml
+
+
+ jitpack.io
+ https://jitpack.io
+
+
+
+
+ com.github.urinaner
+ sejong-auth
+ Tag
+
+```
+
+
+---
+
+## 프로젝트 구조
+
+```
+SejongAuth/
+├── src/
+│ ├── main/
+│ │ ├── java/org/yj/sejongauth/controller/Sj.java
+│ │ ├── java/org/yj/sejongauth/domain/AuthService.java
+│ │ ├── java/org/yj/sejongauth/domain/LoginReq.java
+│ │ ├── java/org/yj/sejongauth/domain/ProfileRes.java
+│ │ └── java/org/yj/sejongauth/domain/ProfileService.java
+│ └── test/
+│ └── java/org/yj/sejongauth/controller/AuthControllerTest.java
+└── build.gradle
+```
+
+---
+
+## 사용법
+
+### 1. `LoginRequestDto` 생성
+
+로그인 요청 객체를 생성합니다.
+
+```java
+LoginRequestDto loginRequestDto = new LoginRequestDto("userId", "password");
+```
+
+### 2. 로그인 및 프로필 조회
+
+`Sj.login()` 메서드를 사용하여 로그인 및 프로필 정보를 조회할 수 있습니다.
+
+```java
+ProfileRes profile = Sj.login(loginReq);
+System.out.println("User profile: " + profile);
+```
+
+---
+
+## 예제
+
+아래는 `Sj.login` 메서드를 호출하여 간단히 로그인과 프로필 조회를 수행하는 예제입니다.
+
+```java
+
+public class Main {
+ public static void main(String[] args) {
+ LoginReq loginReq = new LoginReq("testUser", "testPassword");
+
+ try {
+ ProfileRes profile = Sj.login(loginReq);
+ System.out.println("Login successful. User profile: " + profile);
+ } catch (RuntimeException e) {
+ System.err.println("Login failed: " + e.getMessage());
+ }
+ }
+}
+```
+
+---
+
+## 테스트
+
+`AuthService` 클래스에서 로그인에 대한 테스트를 실행할 수 있습니다.
+
+
+---
+
+## 주의사항
+
+- **네트워크 연결**: 이 프로그램은 네트워크 연결이 필요합니다.
+- **예외 처리**: 네트워크 오류, 로그인 실패 등의 상황에서 `RuntimeException`이 발생할 수 있습니다.
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index b3cd2f0..0cc6084 100644
--- a/build.gradle
+++ b/build.gradle
@@ -34,6 +34,7 @@ dependencyManagement {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:3.1.2'
+ implementation 'org.jsoup:jsoup:1.14.3'
compileOnly 'org.projectlombok:lombok:1.18.26'
annotationProcessor 'org.projectlombok:lombok:1.18.26'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
diff --git a/src/main/java/org/yj/sejongauth/Test.java b/src/main/java/org/yj/sejongauth/Test.java
deleted file mode 100644
index 5d9d84e..0000000
--- a/src/main/java/org/yj/sejongauth/Test.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.yj.sejongauth;
-
-public class Test {
- public static void Console(){
- System.out.println("test");
- }
-}
diff --git a/src/main/java/org/yj/sejongauth/controller/Sj.java b/src/main/java/org/yj/sejongauth/controller/Sj.java
new file mode 100644
index 0000000..84671da
--- /dev/null
+++ b/src/main/java/org/yj/sejongauth/controller/Sj.java
@@ -0,0 +1,21 @@
+package org.yj.sejongauth.controller;
+
+import org.yj.sejongauth.domain.AuthService;
+import org.yj.sejongauth.domain.LoginReq;
+import org.yj.sejongauth.domain.ProfileRes;
+import org.yj.sejongauth.domain.ProfileService;
+
+public class Sj {
+
+ private static final AuthService authService = new AuthService();
+ private static final ProfileService PROFILE_SERVICE = new ProfileService();
+
+ public static ProfileRes login(LoginReq loginReq) {
+ if (authService.authenticate(loginReq)) {
+ String jsessionId = authService.getJsessionId();
+ return PROFILE_SERVICE.fetchUserProfile(jsessionId);
+ } else {
+ throw new RuntimeException("인증에 실패하였습니다.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/yj/sejongauth/domain/AuthService.java b/src/main/java/org/yj/sejongauth/domain/AuthService.java
new file mode 100644
index 0000000..2c975b4
--- /dev/null
+++ b/src/main/java/org/yj/sejongauth/domain/AuthService.java
@@ -0,0 +1,97 @@
+package org.yj.sejongauth.domain;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+import java.util.Map;
+
+public class AuthService {
+ private String jsessionId;
+ private final String SJ_LOGIN_URL = "https://classic.sejong.ac.kr/userLogin.do";
+ private final String SJ_POTAL_URL = "https://classic.sejong.ac.kr";
+ private final String INVALID_AUTH = "인증이 실패하였습니다.";
+ private final String INVALID_SESSION = "SESSION_ID 가져오는 것을 실패하였습니다.";
+ private final String INVALID_URL = "URL이 유효하지 않습니다.";
+ private final String CONTAINS_HTML = "로그인 정보가 올바르지 않습니다.";
+
+ public boolean authenticate(LoginReq loginReq) {
+ try {
+ fetchJsessionId();
+ return attemptLogin(loginReq);
+ } catch (IOException e) {
+ throw new RuntimeException(INVALID_AUTH);
+ }
+ }
+
+ void fetchJsessionId() throws IOException {
+ try {
+ URI uri = new URI(SJ_POTAL_URL);
+ HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
+ connection.setRequestMethod("GET");
+ connection.connect();
+
+ Map> headers = connection.getHeaderFields();
+ List cookies = headers.get("Set-Cookie");
+
+ if (cookies != null) {
+ for (String cookie : cookies) {
+ if (cookie.startsWith("JSESSIONID")) {
+ jsessionId = cookie.split(";")[0].split("=")[1];
+ break;
+ }
+ }
+ }
+
+ if (jsessionId == null) {
+ throw new RuntimeException(INVALID_SESSION);
+ }
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(INVALID_URL);
+ }
+ }
+
+ private boolean attemptLogin(LoginReq loginReq) throws IOException {
+ try {
+ URI uri = new URI(SJ_LOGIN_URL);
+ HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
+ connection.setRequestMethod("POST");
+ connection.setRequestProperty("Cookie", "JSESSIONID=" + jsessionId);
+ connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+ connection.setDoOutput(true);
+
+ String postData = "userId=" + loginReq.getUserId() + "&password=" + loginReq.getPassword();
+ try (OutputStream os = connection.getOutputStream()) {
+ byte[] input = postData.getBytes("utf-8");
+ os.write(input, 0, input.length);
+ }
+
+ int responseCode = connection.getResponseCode();
+ System.out.println(responseCode);
+ String responseMessage = readResponse(connection);
+ System.out.println(responseMessage);
+ return responseCode == 302 && !responseMessage.contains(CONTAINS_HTML);
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(INVALID_URL);
+ }
+ }
+
+ private String readResponse(HttpURLConnection connection) throws IOException {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
+ StringBuilder response = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ response.append(line);
+ }
+ return response.toString();
+ }
+ }
+
+ public String getJsessionId() {
+ return jsessionId;
+ }
+}
diff --git a/src/main/java/org/yj/sejongauth/domain/LoginReq.java b/src/main/java/org/yj/sejongauth/domain/LoginReq.java
new file mode 100644
index 0000000..11a29dd
--- /dev/null
+++ b/src/main/java/org/yj/sejongauth/domain/LoginReq.java
@@ -0,0 +1,19 @@
+package org.yj.sejongauth.domain;
+
+public class LoginReq {
+ private final String userId;
+ private final String password;
+
+ public LoginReq(String userId, String password) {
+ this.userId = userId;
+ this.password = password;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/yj/sejongauth/domain/ProfileRes.java b/src/main/java/org/yj/sejongauth/domain/ProfileRes.java
new file mode 100644
index 0000000..ef297bc
--- /dev/null
+++ b/src/main/java/org/yj/sejongauth/domain/ProfileRes.java
@@ -0,0 +1,31 @@
+package org.yj.sejongauth.domain;
+
+public class ProfileRes {
+ private final String major;
+ private final String studentCode;
+ private final String name;
+ private final int gradeLevel;
+ private final String userStatus;
+ private final int completedSemesters;
+ private final int verifiedSemesters;
+
+ public ProfileRes(String major, String studentCode, String name,
+ int gradeLevel, String userStatus,
+ int completedSemesters, int verifiedSemesters) {
+ this.major = major;
+ this.studentCode = studentCode;
+ this.name = name;
+ this.gradeLevel = gradeLevel;
+ this.userStatus = userStatus;
+ this.completedSemesters = completedSemesters;
+ this.verifiedSemesters = verifiedSemesters;
+ }
+
+ public String getMajor() { return major; }
+ public String getStudentCode() { return studentCode; }
+ public String getName() { return name; }
+ public int getGradeLevel() { return gradeLevel; }
+ public String getUserStatus() { return userStatus; }
+ public int getCompletedSemesters() { return completedSemesters; }
+ public int getVerifiedSemesters() { return verifiedSemesters; }
+}
\ No newline at end of file
diff --git a/src/main/java/org/yj/sejongauth/domain/ProfileService.java b/src/main/java/org/yj/sejongauth/domain/ProfileService.java
new file mode 100644
index 0000000..67b9c8a
--- /dev/null
+++ b/src/main/java/org/yj/sejongauth/domain/ProfileService.java
@@ -0,0 +1,51 @@
+package org.yj.sejongauth.domain;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+
+public class ProfileService {
+ private final String PROFILE_URL = "http://classic.sejong.ac.kr/userCertStatus.do?menuInfoId=MAIN_02_05";
+ private final String FAIDED_PROFILE = "정보 조회에 실패하였습니다.";
+
+ public ProfileRes fetchUserProfile(String jsessionId) {
+ try {
+ URL url = new URL(PROFILE_URL);
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod("GET");
+ connection.setRequestProperty("Cookie", "JSESSIONID=" + jsessionId);
+
+ Document doc = Jsoup.parse(readResponse(connection));
+ return parseProfileFromHtml(doc);
+ } catch (IOException e) {
+ throw new RuntimeException(FAIDED_PROFILE);
+ }
+ }
+
+ private String readResponse(HttpURLConnection connection) throws IOException {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
+ StringBuilder response = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ response.append(line);
+ }
+ return response.toString();
+ }
+ }
+
+ private ProfileRes parseProfileFromHtml(Document document) {
+ String major = document.select("div.contentWrap li dl dd").get(0).text();
+ String studentCode = document.select("div.contentWrap li dl dd").get(1).text();
+ String name = document.select("div.contentWrap li dl dd").get(2).text();
+ int gradeLevel = Integer.parseInt(document.select("div.contentWrap li dl dd").get(3).text().split(" ")[0]);
+ String userStatus = document.select("div.contentWrap li dl dd").get(4).text();
+ int completedSemesters = Integer.parseInt(document.select("div.contentWrap li dl dd").get(5).text().split(" ")[0]);
+ int verifiedSemesters = Integer.parseInt(document.select("div.contentWrap li dl dd").get(6).text().split(" ")[0]);
+
+ return new ProfileRes(major, studentCode, name, gradeLevel, userStatus, completedSemesters, verifiedSemesters);
+ }
+}
diff --git a/src/test/java/org/yj/sejongauth/domain/AuthServiceTest.java b/src/test/java/org/yj/sejongauth/domain/AuthServiceTest.java
new file mode 100644
index 0000000..ebffb62
--- /dev/null
+++ b/src/test/java/org/yj/sejongauth/domain/AuthServiceTest.java
@@ -0,0 +1,44 @@
+package org.yj.sejongauth.domain;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class AuthServiceTest {
+
+ private AuthService authService;
+
+ @BeforeEach
+ void setUp() {
+ authService = new AuthService();
+ }
+
+ @Test
+ void testAuthenticate_Success() throws IOException {
+ // Given
+ LoginReq loginReq = new LoginReq("20003210", "991027zZ!");
+ authService.fetchJsessionId();
+
+ //when
+ boolean isAuthenticated = authService.authenticate(loginReq);
+
+ // then
+ assertTrue(isAuthenticated);
+ }
+
+ @Test
+ void testAuthenticate_Failure() throws IOException {
+ // Given
+ LoginReq loginReq = new LoginReq("invalidUser", "invalidPassword");
+
+ // When
+ boolean isAuthenticated = authService.authenticate(loginReq);
+
+ // Then
+ assertFalse(isAuthenticated);
+ }
+}
diff --git a/src/test/java/org/yj/sejongauth/domain/ProfileServiceTest.java b/src/test/java/org/yj/sejongauth/domain/ProfileServiceTest.java
new file mode 100644
index 0000000..da82637
--- /dev/null
+++ b/src/test/java/org/yj/sejongauth/domain/ProfileServiceTest.java
@@ -0,0 +1,42 @@
+package org.yj.sejongauth.domain;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class ProfileServiceTest {
+
+ private ProfileService profileService;
+
+ @BeforeEach
+ void setUp() {
+ profileService = new ProfileService();
+ }
+
+ @Test
+ @Disabled
+ void testFetchUserProfile_Success() {
+ // Given
+ String jsessionId = "valid-session-id";
+
+ // When
+ ProfileRes profile = profileService.fetchUserProfile(jsessionId);
+
+ // Then
+ assertNotNull(profile);
+ assertEquals("testMajor", profile.getMajor());
+ }
+
+ @Test
+ void testFetchUserProfile_InvalidSession() {
+ // Given
+ String jsessionId = "invalid-session-id";
+
+ // When & Then
+ assertThrows(RuntimeException.class, () -> {
+ profileService.fetchUserProfile(jsessionId);
+ });
+ }
+}