<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>chaedud_02.log</title>
        <link>https://velog.io/</link>
        <description>백엔드 개발⭐</description>
        <lastBuildDate>Thu, 27 Mar 2025 05:59:37 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>chaedud_02.log</title>
            <url>https://velog.velcdn.com/images/chaedud_02/profile/9c9a9865-3209-4132-ac35-82fdfe4210c5/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. chaedud_02.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/chaedud_02" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Redis]]></title>
            <link>https://velog.io/@chaedud_02/Redis</link>
            <guid>https://velog.io/@chaedud_02/Redis</guid>
            <pubDate>Thu, 27 Mar 2025 05:59:37 GMT</pubDate>
            <description><![CDATA[<h2 id="redisremote-dictionary-server">Redis(Remote Dictionary Server)</h2>
<h3 id="개념">개념</h3>
<ul>
<li>다수의 서버가 공유하는 해시테이블</li>
<li>각각의 서버 안에 로컬하게 존재하는 것이 아니라, 개별적인 원격상 존재해 다수의 서버가 공통 사용</li>
<li>애플리케이션에서 세션을 활용하면 사용자가 로그인한 상태를 유지하고, 특정 요청에 대한 정보 저장<br>

</li>
</ul>
<h3 id="특징">특징</h3>
<ul>
<li>오픈소스 인메모리 데이터 저장소</li>
<li>싱글 스레드 기반</li>
<li>클러스터 모드 지원</li>
<li>휘발성 데이터를 저장하지만 RDB와 AOF라는 특성을 통해 안전하게 영속적 관리</li>
<li>Pub/Sub과 같은 기술이 자체적으로 구현</li>
</ul>
<blockquote>
<p>RDB(RedisDataBase)
-&gt; 특정 기간 스냅샷을 생성하는 기술
-&gt; 장애가 발생하면 특정 시점에 스냅샷으로 빠르게 캐시를 되돌리거나 동일한 데이터를 가진 캐시를 복제
-&gt; 스냅샷의 특성상 스냅샷이 생성되기 이전의 일부 데이터는 유실 위험
AOF(AppendOnlyFile)
-&gt; 레디스에 적용되는 write 작업을 모두 log로 저장하는 방식
<br></p>
</blockquote>
<h3 id="흐름">흐름</h3>
<ol>
<li>클라이언트 요청</li>
</ol>
<p>-&gt; 사용자가 웹사이트에 접속하여 로그인 등의 요청을 보냄
-&gt; 브라우저는 쿠키를 포함할 수도 있고, 포함하지 않을 수도 있음
2. 서버 세션 생성
-&gt; 서버는 해당 사용자에 대한 고유 세션 생성
-&gt; 세션 데이터를 저장소에 저장
3. 클라이언트에게 세션 ID 전달
-&gt; 서버는 세션 ID를 응답의 Set-Cookie 헤더를 통해 클라이언트에게 전달
-&gt; 클라이언트는 이를 쿠키에 저장
4. 클라이언트의 이후 요청
-&gt; 클라이언트가 요청을 보낼 때, 저장된 세션 ID가 포함된 쿠키를 함께 전송
<br></p>
<h2 id="세션-테스트">세션 테스트</h2>
<h3 id="로컬">로컬</h3>
<pre><code>wsl --install ##레디스 실행환경을 위한 작업

redis-cli ##레디스 실행

KEYS * ##현재 저장된 세션 값들

flushall ##기존 세션값 모두 지우기</code></pre><p><img src="https://velog.velcdn.com/images/chaedud_02/post/f401d960-993d-4082-9cfb-a97cac798dfc/image.png" alt=""></p>
<ul>
<li>이런 식으로 세션 ID값이 생성</li>
<li>WAS쪽에서도 세션 ID값이 동일한 것으로 확인 <br>

</li>
</ul>
<h3 id="개인적인-생각">개인적인 생각</h3>
<ul>
<li>웹브라우저 쿠키 세션 ID값과 Redis 세션 ID값, WAS 세션 ID값을 다 확인한 결과, 웹브라우저만 구조값이 달랐음</li>
<li>무작정 이유를 찾아보아도 떠오르는 해결방법이 없었음</li>
<li>yml설정파일에 구조도 맞춰보고 이름도 변경했지만 별 다른게 없었음</li>
<li>이유는 ?  </li>
<li>찾아본 결과 디코딩 해야 했음</li>
<li>브라우저 값을 디코딩하면 세션과 동일한 값이 나옴 </li>
<li>결국 세개의 값은 모두 동일 </li>
</ul>
<br>

<p><strong>참고자료</strong>
<a href="https://cobinding.tistory.com/234#Redis%20%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-1">https://cobinding.tistory.com/234#Redis%20%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-1</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NGINX]]></title>
            <link>https://velog.io/@chaedud_02/NGINX</link>
            <guid>https://velog.io/@chaedud_02/NGINX</guid>
            <pubDate>Tue, 04 Mar 2025 06:07:14 GMT</pubDate>
            <description><![CDATA[<h3 id="nginx">NGINX</h3>
<ul>
<li>매우 강력하고 인기있는 웹 서버 소프트웨어</li>
<li>고성능과 확장성을 제공하는 웹 서버 및 리버스 프록시 서버 역할</li>
</ul>
<blockquote>
<p><strong>웹 서버(WS)</strong>
주로 클라이언트가 보낸 HTTP 요청을 처리하고, 요청된 웹 페이지나 리소스를 클라이언트에게 전송
<strong>클라리언트(Client)</strong>
서비스를 이용하기 위해 네트워크를 통해 요청을 보내는 주체를 의미
<strong>WAS(Web Application Server)</strong>
클라이언트 요청에 대해 동적인 처리 담당하는 영역
웹 서버와 달리 애플리케이션 로직 실행 가능하도록 구성</p>
</blockquote>
<br>

<h3 id="구조">구조</h3>
<ul>
<li>높은 성능을 가질 수 있는 이유는 요청에 응답하기 위한 비동기식 이벤트 기반 구조</li>
<li>구글 검색엔진에서 Nginx와 함께 상위 랭킹을 차지하고 있는 Apache는 nginx와 다르게 스레드/프로세스 기반 구조</li>
</ul>
<br>

<h3 id="nignx-동작">NIGNX 동작</h3>
<p><img src="https://velog.velcdn.com/images/chaedud_02/post/b7bfc950-be0e-4a4c-b5ee-3c5516430613/image.png" alt=""></p>
<ul>
<li><p>정적 콘텐츠 제공</p>
</li>
<li><blockquote>
<p>HTML, CSS, JS, Image 등의 정적 파일을 직접 제공</p>
</blockquote>
</li>
<li><p>리버스 프록시
  -&gt; 클라이언트의 요청을 받아서 다른 서버로 전달하고, 그 응답을 클라이언트에게 다시 전달
  -&gt; 여러 백엔드 서버로의 요청을 분배하는 로드 밸런싱을 지원해 서버의 부하를 분산</p>
</li>
<li><p>로드 밸런싱
  -&gt; 여러 개의 백엔드 서버에 트래픽을 분산
  -&gt; 라운드 로빈, 최소 연결 등의 방식 지원</p>
<blockquote>
<p><strong>라운드 로빈(RR)</strong>
시분할 시스템을 위해 설계된 선점형 스케줄링
프로게스들 사이에 우선순위를 두지 않고, 순서대로 시간단위로 CPU를 할당하는 방식의 CPU 스케줄링</p>
</blockquote>
</li>
<li><p>HTTP 캐싱
  -&gt; 정적 콘텐츠나 API 응답을 캐싱하여 성능 최적화</p>
</li>
<li><p>보안 및 HTTPS 설정
  -&gt; SSL/TLS 적용ㅇ을 통한 보안 강화</p>
<blockquote>
<p><strong>SSL/TLS 암호화</strong>
보안 연결을 위한 프로토콜로,  HTTP로 웹사이트에 안전하게 접근</p>
</blockquote>
</li>
</ul>
<br>


<h3 id="설정-파일docker---nginx">설정 파일(Docker -&gt; Nginx)</h3>
<pre><code>### bash
mkdir -p /docker/nginx/conf /docker/nginx/certs /docker/nginx/logs
nano /docker/nginx/conf/default.conf

### .conf
server {
    listen 80;
    server_name localhost;

    location / {
        proxy_pass http://gitlab:80;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    error_page 404 /404.html;
    location = /404.html {
        root /usr/share/nginx/html;
    }

    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
        root /usr/share/nginx/html;
    }
}
</code></pre><br>

<p><strong>참고자료</strong>
<a href="https://jwprogramming.tistory.com/17">https://jwprogramming.tistory.com/17</a>
<a href="https://eun-jeong.tistory.com/17">https://eun-jeong.tistory.com/17</a>
<a href="https://blog.naver.com/gi_balja/223028077537">https://blog.naver.com/gi_balja/223028077537</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[그룹 웨어 - 휴가 관리(4)]]></title>
            <link>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9-%EC%9B%A8%EC%96%B4-%ED%9C%B4%EA%B0%80-%EA%B4%80%EB%A6%AC4</link>
            <guid>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9-%EC%9B%A8%EC%96%B4-%ED%9C%B4%EA%B0%80-%EA%B4%80%EB%A6%AC4</guid>
            <pubDate>Fri, 06 Dec 2024 15:48:19 GMT</pubDate>
            <description><![CDATA[<h3 id="휴가-지급-기준-지정일-및-입사일-구성">휴가 지급 기준 지정일 및 입사일 구성</h3>
<ul>
<li>직원에게 휴가 지급 기준을 설정</li>
<li>기업 방침마다 설정할 수 있게 두 가지의 조건을 지정</li>
<li>지정일 : 지정한 일(2025.01.01)을 설정하면, 모든 직원에게 리셋 후에 연차별 지급 </li>
<li><blockquote>
<p><em>1년 미만은 제외</em></p>
</blockquote>
</li>
<li>입사일 : 직원마다 입사일 기준이 되면, 리셋 후에 새로 지급</li>
<li>스케쥴링을 통해 조건에 따라 실행되게 설정<br>

</li>
</ul>
<h3 id="휴가-지급-기준-지정일">휴가 지급 기준 지정일</h3>
<ul>
<li><p>입사일 기준 체크박스와 지정일 체크박스가 동시에 처리 되지 않음</p>
</li>
<li><p>지정일 같은 경우, 지정날짜 선택 필수</p>
<pre><code>//휴가 기준 설정
try {
      String type = (String) payload.get(&quot;type&quot;);
      VacationStandardDto dto = new VacationStandardDto();

      if (&quot;designated&quot;.equals(type)) {
          String designatedDate = (String) payload.get(&quot;designatedDate&quot;); //지정날짜
          dto.setVacation_standard_status(1);
          dto.setVacation_standard_date(designatedDate);
      }else if(&quot;joined&quot;.equals(type)) {
          dto.setVacation_standard_status(0);
      }

      if (vacationService.checkStandard(dto) &gt; 0) {
          resultMap.put(&quot;res_code&quot;, &quot;200&quot;);
          resultMap.put(&quot;res_msg&quot;, &quot;휴가 기준이 설정되었습니다.&quot;);
      }
  } catch (Exception e) {
      e.printStackTrace();
      resultMap.put(&quot;res_code&quot;, &quot;404&quot;);
      resultMap.put(&quot;res_msg&quot;, &quot;처리 중 오류가 발생했습니다.&quot;);
  }</code></pre></li>
<li><p>타입별로 기준 값 설정하기</p>
</li>
<li><p>이러한 기준을 토대로 스케쥴링 이용하여 직원에게 휴가 지급하도록 구현함</p>
<br>

</li>
</ul>
<h3 id="마치며">마치며</h3>
<ul>
<li>프로젝트를 구성하면서, 많은 시행착오도 겪고 성장했던 시간</li>
<li>몰랐던 서비스 로직에 대해 더욱 정확히 사용할 수 있게 되었음</li>
<li>디비 구성이나 Mybatis를 사용하면서 내가 잘 이용할 수 있는 방향성을 잡을 수 있었음</li>
<li>아직은 정확히 구현하고 구성하는데 부족하지만, 앞으로는 새로운 기술도 접하고 이용하면서 보완해 나아갈 예정</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[그룹 웨어 - 휴가 관리(3)]]></title>
            <link>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9-%EC%9B%A8%EC%96%B4-%ED%9C%B4%EA%B0%80-%EA%B4%80%EB%A6%AC3</link>
            <guid>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9-%EC%9B%A8%EC%96%B4-%ED%9C%B4%EA%B0%80-%EA%B4%80%EB%A6%AC3</guid>
            <pubDate>Mon, 25 Nov 2024 14:48:22 GMT</pubDate>
            <description><![CDATA[<h3 id="1년-미만-월차-지급-스케줄링-조건">1년 미만 월차 지급 스케줄링 조건</h3>
<ul>
<li>1년 미만 재직자와 1년 이상 재직자를 구분</li>
<li>1년 미만 재직자에게 매일 월차 지급 날짜 확인 후, 한 달 초과 시 +1</li>
<li>만약 처음 지급일 경우, 입사일 기준 한 달 후에 +1하고 지급 날짜 업데이트</li>
<li>이후 업데이트 날짜 기준으로 한 달 초과 +1 하도록 조건<br>


</li>
</ul>
<h3 id="1년-미만-월차-지급">1년 미만 월차 지급</h3>
<pre><code>if (vacationService.countCheckOneYear() == 1) {
    List&lt;MemberDto&gt; underOneYearMembers = memberService.selectUnderYearMember(1);
    for (MemberDto dto : underOneYearMembers) {
        // 휴가일이 설정되지 않은 경우
        if (dto.getMember_vacation_date() == null || dto.getMember_vacation_date().isEmpty()) {
            if (firstVacation(dto.getMember_hire_date())) {
                double monthDif = (double) Period.between(LocalDate.parse(dto.getMember_hire_date()), LocalDate.now()).toTotalMonths();
                vacationService.incrementVacation(dto.getMember_no(), monthDif);
            }
        } 
        // 휴가일이 설정된 경우
        else {
            if (monthVacation(dto.getMember_vacation_date())) {
                vacationService.incrementVacation(dto.getMember_no(), 1.0);
            }
        }
    }
}

//입사 후 처음 지급 받는 경우
private boolean firstVacation(String joiningDate) {
    try {
        LocalDate joinDate = LocalDate.parse(joiningDate);
        return Period.between(joinDate, LocalDate.now()).toTotalMonths() &gt;= 1;
    } catch (DateTimeParseException e) {
        // 날짜 파싱 실패 시 예외 처리
        return false;
    }
}

//마지막 휴가 지급일 기준으로 한 달 경과
private boolean monthVacation(String lastVacationDateStr) {
    try {
        if (lastVacationDateStr == null || lastVacationDateStr.isEmpty()) {
            return false;
        }
        LocalDate lastVacationDate = LocalDate.parse(lastVacationDateStr);
        return Period.between(lastVacationDate, LocalDate.now()).toTotalMonths() &gt;= 1;
    } catch (DateTimeParseException e) {
        // 날짜 파싱 실패 시 예외 처리
        return false;
    }
}
</code></pre><ul>
<li>반복문 안에 있는 조건을 통해 처음 월차를 지급 받는지와 다음 월차를 지급 받는지 구분하여 처리</li>
<li>입사일 기준으로 한 달 초과하면 그 값을 서비스 로직을 통해 디비에 저장</li>
<li>기존 지급한 이력이 있는 경우,  +1 하도록 구성</li>
<li>처음 지급 받는 것도 +1로 구성하여도 좋지만, 테스트 때 멤버 테이블에 입사일을 임의로 지정하고 테스트했기에 차이 값으로 구현<br>

</li>
</ul>
<h3 id="연차별-휴가-개수-지급-조건">연차별 휴가 개수 지급 조건</h3>
<ul>
<li>지정일 기준인지, 입사일 기준인지에 대해 다르게 구성</li>
<li>연차별로 지정한 휴가 개수를 해당 사원 입사일 기준으로 측정하여 지급<br>

</li>
</ul>
<h3 id="연차별-휴가-개수">연차별 휴가 개수</h3>
<pre><code>if (vacationService.countCheckOneYear() == 1 || vacationService.countCheckOneYear() == 0) {
    List&lt;MemberDto&gt; overOneYearMembers = memberService.selectUnderYearMember(0);
    for (MemberDto dto : overOneYearMembers) {
        String vacationDate = vacationService.selectVacationDesignated(1); // 지정일 조회
        int vacationStandardStatus = vacationService.selectVacationStandardStatus();
        String referenceDate = (vacationDate != null) ? vacationDate : dto.getMember_hire_date();

        if (resetDate(referenceDate, vacationStandardStatus)) {
            int yearSinceJoin = Period.between(LocalDate.parse(dto.getMember_hire_date()), LocalDate.now()).getYears();
            double vacationDay = getVacationDaysByYears(yearSinceJoin);
            vacationService.resetVacation(dto.getMember_no(), vacationDay);
        }
    }
}

// 기준일에 맞춰서 휴가 리셋 시점 확인
private boolean resetDate(String referenceDate, int status) {
    try {
        LocalDate refDate = LocalDate.parse(referenceDate);

        if (status == 1) {
            // 지정일 기준
            return LocalDate.now().isEqual(refDate) || LocalDate.now().isAfter(refDate);
        } else if (status == 0) {
            // 입사일 기준
            return LocalDate.now().isEqual(refDate) || LocalDate.now().isAfter(refDate);
        }

        return false;


    } catch (DateTimeParseException e) {
        return false;
    }
}

// 연차에 따른 휴가 개수 반환
private int getVacationDaysByYears(int years) {
    // 휴가 일수 저장된 테이블에서 연차에 따른 휴가 개수 조회
    int count = vacationService.contVacationYear(years);
    return count;
}</code></pre><ul>
<li>1년 이상 사원의 리스트를 가져옴</li>
<li>지정일이 있는지 확인하고 없다면 입사일 기준으로 설정</li>
<li>휴가 초기화 조건을 확인</li>
<li>근속 연수에 따라 연차별 휴가 개수를 가져와서 지급<br>

</li>
</ul>
<h3 id="트러블-슈팅">트러블 슈팅</h3>
<ul>
<li>기본적으로 스케줄링이 여러 개 사용되어야 하고, 조건에 따라 다른 값들을 입력해야 하기 때문에 조건 분리에 대한 오류가 많았음</li>
<li>하나씩 플로우 차트도 그리고 값을 넣어서 테스트를 해봄</li>
<li>큰 조건 먼저 나누고 그 조건 안에 세부 조건을 나누는 식이 접근도 편하고 구현도 편했음</li>
<li>특히 입사일 기준과 지정일 기준에 대한 조건은 상태값을 통해 분리하기 위해 서비스 들렸다 가져와야 하는 구성이 어려웠음</li>
<li>이야기를 작성하듯이 코드를 구현하여 해결</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[그룹웨어 - 휴가 관리(2)]]></title>
            <link>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%ED%9C%B4%EA%B0%80-%EA%B4%80%EB%A6%AC2</link>
            <guid>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%ED%9C%B4%EA%B0%80-%EA%B4%80%EB%A6%AC2</guid>
            <pubDate>Sun, 24 Nov 2024 13:39:37 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/chaedud_02/post/0724192a-dbeb-44af-bcb1-6976779cb655/image.png" alt=""></p>
<h3 id="휴가-지정-구성">휴가 지정 구성</h3>
<ul>
<li>법적으로 25년 이상의 개수는 같은 수를 지급하기에 25년차 이상부터는 같은 개수를 지급</li>
<li>년차별로 회사마다 증가하는 개수가 다를 수 있으므로, 설정가능하게 구성</li>
<li>등록된 후, 추후 수정가능하게 구성</li>
<li>만약 값이 등록되지 않게 되면, 0으로 일괄 지정 </li>
</ul>
<h3 id="휴가-지정">휴가 지정</h3>
<pre><code>//Vacation
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name=&quot;vacation_no&quot;)
private Long vacationNo;

@Column(name=&quot;vacation_year&quot;)
private int vacationYear; // 년차 기준 

@Column(name=&quot;vacation_annual_leave&quot;)
private int vacationAnnualLeave;// 년차에 따른 지급 개수

@Column(name=&quot;vacation_create_date&quot;)
@CreationTimestamp
private LocalDateTime vacationCreateDate;

@Column(name=&quot;member_no&quot;)
private Long memberNo;

//vacation.js
// 연차 개수 제출 시 처리
document.addEventListener(&#39;DOMContentLoaded&#39;, () =&gt; {
    const form = document.getElementById(&quot;vacationForm&quot;);
    const csrfToken = document.querySelector(&#39;input[name=&quot;_csrf&quot;]&#39;).value;

    form.addEventListener(&#39;submit&#39;, function(event) {
        event.preventDefault();

        Swal.fire({
            text: &#39;입력되지 않은 연차의 개수는 0으로 지정됩니다. 계속하시겠습니까?&#39;,
            icon: &#39;warning&#39;,
            showCancelButton: true,
            confirmButtonText: &#39;확인&#39;,
            cancelButtonText: &#39;취소&#39;,
            customClass: {
                 confirmButton: &#39;custom-confirm-button&#39;,
                 cancelButton: &#39;custom-cancel-button&#39;
            }
        }).then((result) =&gt; {
            if (result.isConfirmed) {
                // 사용자가 확인했을 때 처리 진행
                const memberNo = document.getElementById(&#39;memberNo&#39;).value;
                const yearInputs = document.querySelectorAll(&#39;[id^=&quot;year&quot;]&#39;);
                const countVacation = document.getElementById(&#39;countVacation&#39;).value;
                const vacationPkElements = document.querySelectorAll(&#39;[id^=&quot;vacationPk&quot;]&#39;);

                let count = 1;
                const vacationData = {};
                const vacationPkData = [];

                if (countVacation &gt; 0) {
                    vacationPkElements.forEach(input =&gt; {
                        if (input.value) {
                            vacationPkData.push(input.value);
                        }
                    });
                    yearInputs.forEach(input =&gt; {
                        const year = input.id.replace(&#39;year&#39;, &#39;&#39;);
                        const vacationDays = input.value;

                        // 0으로 지정
                        vacationData[count] = vacationDays.trim() === &quot;&quot; ? 0 : parseInt(vacationDays);
                        count++;
                    });

                    const requestData = {
                        memberNo: memberNo,
                        vacationData: vacationData,
                        lessThanOneYear: lessThanOneYear,
                        countVacation: countVacation,
                        count: count,
                        vacationPkData: vacationPkData
                    };

                    fetch(&#39;/vacation/addVacationAction&#39;, {
                        method: &#39;POST&#39;,
                        headers: {
                            &#39;Content-Type&#39;: &#39;application/json&#39;,
                            &#39;X-CSRF-TOKEN&#39;: csrfToken
                        },
                        body: JSON.stringify(requestData)
                    })
                    .then(response =&gt; {
                        if (!response.ok) {
                            throw new Error(&#39;Network response was not ok&#39;);
                        }
                        return response.json();
                    })
                    .then(data =&gt; {
                        if (data.res_code === &#39;200&#39;) {
                            Swal.fire({
                                icon: &#39;success&#39;,
                                text: data.res_msg,
                                confirmButtonText: &quot;닫기&quot;,
                                customClass: {
                                       confirmButton: &#39;custom-confirm-button&#39;

                                 }
                            }).then((result) =&gt; {
                                if (result.isConfirmed) {
                                    location.reload();
                                }
                            });
                        } else {
                            Swal.fire({
                                icon: &#39;error&#39;,
                                text: data.res_msg,
                                confirmButtonText: &quot;닫기&quot;,
                                customClass: {
                                     confirmButton: &#39;custom-confirm-button&#39;

                                }
                            });
                        }
                    })
                    .catch(error =&gt; {
                        Swal.fire({
                            icon: &#39;error&#39;,
                            text: &#39;서버와의 통신 중 오류가 발생했습니다.&#39;,
                            confirmButtonText: &quot;닫기&quot;,
                            customClass: {
                                confirmButton: &#39;custom-confirm-button&#39;
                            }
                        });
                    });
                } else {
                    yearInputs.forEach(input =&gt; {
                        const year = input.id.replace(&#39;year&#39;, &#39;&#39;);
                        const vacationDays = input.value;

                        vacationData[year] = vacationDays.trim() === &quot;&quot; ? 0 : parseInt(vacationDays);
                    });

                    const requestData = {
                        memberNo: memberNo,
                        vacationData: vacationData,
                        lessThanOneYear: lessThanOneYear,
                        countVacation: countVacation
                    };

                    fetch(&#39;/vacation/addVacationAction&#39;, {
                        method: &#39;POST&#39;,
                        headers: {
                            &#39;Content-Type&#39;: &#39;application/json&#39;,
                            &#39;X-CSRF-TOKEN&#39;: csrfToken
                        },
                        body: JSON.stringify(requestData)
                    })
                    .then(response =&gt; {
                        if (!response.ok) {
                            throw new Error(&#39;Network response was not ok&#39;);
                        }
                        return response.json();
                    })
                    .then(data =&gt; {
                        if (data.res_code === &#39;200&#39;) {
                            Swal.fire({
                                icon: &#39;success&#39;,
                                text: data.res_msg,
                                confirmButtonText: &quot;확인&quot;,
                                customClass: {
                                     confirmButton: &#39;custom-confirm-button&#39;

                                }
                            }).then((result) =&gt; {
                                if (result.isConfirmed) {
                                    location.reload();
                                }
                            });
                        } else {
                            Swal.fire({
                                icon: &#39;error&#39;,
                                text: data.res_msg,
                                confirmButtonText: &quot;확인&quot;,
                                customClass: {
                                      confirmButton: &#39;custom-confirm-button&#39;

                                }
                            });
                        }
                    })
                    .catch(error =&gt; {
                        Swal.fire({
                            icon: &#39;error&#39;,
                            text: &#39;서버와의 통신 중 오류가 발생했습니다.&#39;,
                            confirmButtonText: &quot;닫기&quot;,
                            customClass: {
                                 confirmButton: &#39;custom-confirm-button&#39;

                            }
                        });
                    });
                }
            }
        });
    });
});

// vacationController
@PostMapping(&quot;/addVacationAction&quot;)
@ResponseBody
public Map&lt;String, String&gt; addVacation(@RequestBody Map&lt;String, Object&gt; params) {
  Map&lt;String, String&gt; resultMap = new HashMap&lt;&gt;();
  resultMap.put(&quot;res_code&quot;, &quot;404&quot;);
  resultMap.put(&quot;res_msg&quot;, &quot;휴가 생성 중 오류가 발생했습니다.&quot;);

  try {
    Long memberNo = Long.parseLong((String) params.get(&quot;memberNo&quot;));
    // 연차 입력 데이터 처리
    Map&lt;String, Object&gt; vacationData = (Map&lt;String, Object&gt;) params.get(&quot;vacationData&quot;);
    VacationDto dto = new VacationDto();
    dto.setMember_no(memberNo);
    dto.setVacationData(vacationData);

    int count = Integer.parseInt(String.valueOf(params.get(&quot;countVacation&quot;)));

    if(count &gt; 0) {
       List&lt;String&gt; vacationPk = (List&lt;String&gt;) params.get(&quot;vacationPkData&quot;);
       for(int i = 0; i&lt; vacationPk.size(); i++){

              String pk = vacationPk.get(i);
              dto.setVacation_no(Long.parseLong(pk));

            List&lt;Vacation&gt; vacations = dto.toEntities();
            dto.setVacation_no(vacations.get(i).getVacationNo());
             dto.setVacation_annual_leave(vacations.get(i).getVacationAnnualLeave());
            dto.setVacation_year(vacations.get(i).getVacationYear());

            if (vacationService.addVacation(dto) &gt; 0) {
              resultMap.put(&quot;res_code&quot;, &quot;200&quot;);
              resultMap.put(&quot;res_msg&quot;, &quot;휴가 개수가 지정되었습니다.&quot;);
            }
        }

     }
    List&lt;Vacation&gt; vacations = dto.toEntities();

      for (Vacation vacation : vacations) {
        dto.setVacation_year(vacation.getVacationYear());
        dto.setVacation_annual_leave(vacation.getVacationAnnualLeave());

        if (vacationService.addVacation(dto) &gt; 0) {
           resultMap.put(&quot;res_code&quot;, &quot;200&quot;);
           resultMap.put(&quot;res_msg&quot;, &quot;휴가 개수가 지정되었습니다.&quot;);
        }

         }

} catch (Exception e) {
    e.printStackTrace();
    resultMap.put(&quot;res_code&quot;, &quot;404&quot;);
    resultMap.put(&quot;res_msg&quot;, &quot;처리 중 오류가 발생했습니다.&quot;);
}
    return resultMap;
}
</code></pre><ul>
<li>js는 Swal을 사용하므로 코드가 길지만, 중요한 부분을 확인하면 초기 입력과 수정이 동시에 한 form에서 가능해야 하기 때문에 html에서 입력된 값의 개수에 따라 조건을 분류</li>
<li>만약 새로 입력하는 경우에는, 다 처음 값을 넘겨야 하기 때문에 yearInput만 확인</li>
<li>기존에 있는 값을 변경하게 되면, pk값이 필요하고 이를 가지고 jpa 수정(save)이 바로 가능하다</li>
<li>그래서 pk값들을 따라 배열에 저장하여 넘긴다<br>

</li>
</ul>
<h3 id="1년-미만-월차-지급-구성">1년 미만 월차 지급 구성</h3>
<ul>
<li>member 데이터에서 현재 날짜에서 입사일 기준으로 차이가 1년이 되지 않는 직원들은 1년 미만 상태값을 변경</li>
<li>그 점을 이용하여 다른 1년 이상 재직자는 상태값 0으로 유지</li>
<li>스케쥴링을 통해 매일 1년 미만 재직자의 상태에 따라 개수 지급</li>
<li>입사일 기준으로 첫 한 달이 지나게 되면, 값을 +1하고 업데이트 날짜에 지급한 날짜 저장</li>
<li>이후 지급은 업데이트 날짜를 기준으로 +1 하고, 또 날짜 업데이트<br>

</li>
</ul>
<h3 id="1년-미만-월차-지급">1년 미만 월차 지급</h3>
<pre><code>//vacationOneUnder
//지급 여부 초기 설정
//선택 시, 상태값 1로 저장
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name=&quot;vacation_under_no&quot;)
private Long vacationUnderNo;

@Column(name=&quot;vacation_under_status&quot;)
private int vacationUnderStatus;

//vacation.html
&lt;form id=&quot;oneUnderForm&quot; class=&quot;oneUnderForm&quot;&gt;
    &lt;input type=&quot;hidden&quot; th:name=&quot;${_csrf.parameterName}&quot; th:value=&quot;${_csrf.token}&quot;/&gt;
    &lt;div&gt;
        &lt;div class=&quot;vacation_ele1&quot;&gt;
            &lt;p style=&quot;font-weight: 550;&quot;&gt;1년 미만 월차 지급 여부&lt;/p&gt;
            &lt;div class=&quot;checkButton&quot; style=&quot;text-align: center; margin-top: 20px;&quot;&gt;
                &lt;!-- countCheckOneYear가 0보다 크면 버튼을 비활성화 --&gt;
                &lt;button type=&quot;submit&quot; th:disabled=&quot;${countCheckOneYear &gt; 0}&quot;&gt;등록&lt;/button&gt;
            &lt;/div&gt;
        &lt;/div&gt;
        &lt;!-- 분리되는 부분 --&gt;
        &lt;div&gt;
            &lt;p style=&quot;color: red; font-size: 15px; padding-left: 20px; padding-right: 15px;&quot;&gt;* 최초 설정만 가능&lt;/p&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div&gt;
        &lt;div&gt;
            &lt;!-- 체크박스 비활성화 처리 --&gt;
            &lt;input type=&quot;checkbox&quot; id=&quot;lessThanOneYear&quot; name=&quot;lessThanOneYear&quot; value=&quot;true&quot;
                                   th:disabled=&quot;${countCheckOneYear &gt; 0}&quot;
                                   th:checked=&quot;${countCheckOneYear &gt; 0}&quot;&gt;
            &lt;label for=&quot;lessThanOneYear&quot;&gt;1년 미만&lt;/label&gt;
         &lt;/div&gt;
    &lt;/div&gt;
&lt;/form&gt;

//vacationSchedulerService
@Transactional
public void updateVacationStatus() {
    LocalDate now = LocalDate.now();

    memberRepository.findAll().forEach(member -&gt; {
        String hire= member.getMemberHireDate();
        LocalDate hireDate = LocalDate.parse(hire, DATE_FORMATTER);
        long monthsBetween = ChronoUnit.MONTHS.between(hireDate, now);

        if (monthsBetween &lt; 12) {
            member.setMemberOneUnder(1); // 상태를 1로 변경
            memberRepository.save(member);
        } else {
            member.setMemberOneUnder(0); // 1년 이상인 경우 상태를 0으로 변경
            memberRepository.save(member);
        }
    });

}
</code></pre><ul>
<li>스케쥴링을 통해 현재 시간 - 입사일 기준 &lt; 12 이면, 1년 미만 재직자로 상태값 변경하기</li>
<li>나중에 1년미만 재직자가 1년 이상 재직자로 변경될 경우, 상태값 변경도 가능하도록 구성<br>

</li>
</ul>
<h3 id="트러블-슈팅">트러블 슈팅</h3>
<ul>
<li>JPA에서 삽입할 때, save를 사용했는데 수정도 같이 사용이 가능하다는 점을 이용해서 입력폼은 동일하니 수정하면 자동 수정되는 것으로 알았음</li>
<li>하지만 값이 수정되지 않고 오류 발생</li>
<li>이를 확인하고자, save 사용방법을 찾아 확인 후에 pk값을 가지고 있어야 수정이 정상적으로 가능하다는 것을 깨달음</li>
<li>그래서 처음 입력과 기존 입력된 상황을 나누기 위해 count를 하여 vacation 년차 값을 조건으로 다르게 처리되게 구성</li>
<li>1년 미만 재직자 설정할 때, 멤버 값을 따로 빼서 테이블을 구성하려 했으나 나중에 사용자마다 메인에 휴가 개수를 띄울 때에 여러 테이블을 조인해야하는 상황 발생</li>
<li>멤버 테이블에 컬럼 추가로 마무리 함</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[그룹웨어 - 휴가 관리(1)]]></title>
            <link>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%ED%9C%B4%EA%B0%80-%EA%B4%80%EB%A6%AC1</link>
            <guid>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%ED%9C%B4%EA%B0%80-%EA%B4%80%EB%A6%AC1</guid>
            <pubDate>Sun, 10 Nov 2024 12:43:15 GMT</pubDate>
            <description><![CDATA[<h3 id="휴가-관리-기능-설명">휴가 관리 기능 설명</h3>
<ul>
<li>관리자가 휴가를 지급하는 기준을 설정할 수 있는 기능</li>
<li>1년 미만 재직자에게 월차를 자동 지급할 수 있음</li>
<li>1년차부터 년차별로 개수를 지급하는 기준을 설정하고, 조정가능</li>
<li>휴가 종류(반차, 반반차, 연차)를 회사 정책마다 설정 가능</li>
<li>입사일 기준으로 연차 개수가 새로 지급될지, 회사 지정일에 따라 지급될지 구분 가능<br>

</li>
</ul>
<h3 id="테이블-구성">테이블 구성</h3>
<p><img src="https://velog.velcdn.com/images/chaedud_02/post/e4894bd4-14ce-49c5-849e-f22e236f9974/image.png" alt=""></p>
<ul>
<li>fl_vacation 테이블은 연차와 연차에 해당하는 개수를 가지고 있음</li>
<li>fl_vacation_type 테이블은 휴가 종류와 차감 개수를 가지고 있음</li>
<li>fl_vacation_under 테이블은 1년 미만 재직자 월차 지급 여부를 확인</li>
<li>fl_vacation_standard 테이블은 지급 방법(지정일, 입사일)을 가지고 있음<br>

</li>
</ul>
<h3 id="구현하면서-들었던-생각">구현하면서 들었던 생각</h3>
<ul>
<li>그룹웨어를 사용해본적이 없고, 휴가에 대한 조건을 처음부터 지정할 수 있도록 구성해야하기 때문에 고민이 많았음</li>
<li>특히 자동 지급이 되어야 함으로, 지급 조건 구성을 신경써야 했음</li>
<li>과정에 대해 로직 구성이 쉽지 않았는데, 스케쥴링도 그에 따라 다르게 동작하도록 구성해야 함</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[그룹웨어 - 알림 구현(4)]]></title>
            <link>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%844</link>
            <guid>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%844</guid>
            <pubDate>Tue, 05 Nov 2024 05:43:30 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/chaedud_02/post/fe95c7cc-04fc-42ba-a390-d85fdd4794e9/image.gif" alt=""></p>
<h3 id="구성">구성</h3>
<ul>
<li>알림 드롭 메뉴와 알림 실시간 모달창을 클릭하면 해당 기능의 페이지로 이동하도록 해야 함</li>
<li>페이지 이동과 동시에 해당되는 알림 읽음 처리 진행</li>
<li>전자 결재는 상세 페이지로 이동할 수 있도록 구성</li>
</ul>
<br>

<h3 id="클릭-후-이동-기능">클릭 후 이동 기능</h3>
<pre><code>//클릭 후, 페이지 이동 경로
const noficationTypeUrl={
     1: `/api/chat/${headerCurrentMember}`,
     11: `http://localhost:8080/employee/schedule`,
     3 : `/employee/approval/approval_history_vacation_detail/`,
     4 : `/employee/approval/approval_references_vacation_detail/`,
     5 : `/employee/approval/approval_history_vacation_detail/`,
     6 : `/employee/vacationapproval/detail/`,
     7 : `/employee/approval/approval_history_detail/`,
     8 : `/employee/approval/approval_references_detail/`,
     9 : `/employee/approval/approval_history_detail/`,
     10 : `/employee/approval/approval_reject_detail/`,
     14 : `/employee/vacationapproval/detail/`,
     15 : `/employee/approval/approval_progress_detail/`,
     2 : `/employee/document/department/${headerCurrentDepartment}`
}

// 알림핸들러에서 응답이 온 후, 그 해당 알림 div 요소를 클릭해 진행
listItem.addEventListener(&#39;click&#39;, () =&gt; {
    const notificationType = listItem.getAttribute(&#39;data-notification-type&#39;);
    const notificationTypePk = listItem.getAttribute(&#39;data-notification-type-pk&#39;);
    if (notificationType === &#39;3&#39;) {
        window.location.href = noficationTypeUrl[3]+notificationTypePk;
    }
});
</code></pre><ul>
<li>기존처럼 페이지 진입 시, 해당 기능 알림만 읽음 처리 진행하기 때문에 따로 구성하지 않았음</li>
<li>전자 결재는 세부 페이지 이동해야 하기 때문에 + pk값이 추가 </li>
<li>목록 페이지나 기능 전체 페이지로 이동하는 경우는 변경없이 이동<br>

</li>
</ul>
<h3 id="트러블-슈팅">트러블 슈팅</h3>
<ul>
<li>그룹웨어 기능에 대한 깊은 이해도가 없었기에 해당 기능페이지 목록으로 이동</li>
<li>전자결재는 해당 세부페이지로 이동해야 함</li>
<li>url 관련된 변경이 이루어짐</li>
<li>기존 읽음처리처럼 전자 결재 알림 구성을 조금 변경했음</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[그룹웨어 - 알림 구현(3)]]></title>
            <link>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%843</link>
            <guid>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%843</guid>
            <pubDate>Tue, 05 Nov 2024 05:01:51 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/chaedud_02/post/80187c7c-ac37-4907-bc1c-0e3afc58eb38/image.png" alt=""></p>
<h3 id="구성">구성</h3>
<ul>
<li>알림 모달창 뿐만 아니라 다른 알림 리스트가 필요</li>
<li>그리고 안읽음 개수 또한 출력하여 쌓여있는 알림 내용을 확인 가능하도록 구성</li>
<li>일괄 읽음을 통해 읽지 않은 내용을 처리</li>
<li>모달창과 동일하게 내용 구성</li>
<li>실시간 변화가 적용되어야 함<br>

</li>
</ul>
<h3 id="기능-구현">기능 구현</h3>
<pre><code>//통합알림 개수
function bellUnreadCount() {
       fetch(`/api/nofication/unread/${headerCurrentMember}`)
           .then(response =&gt; response.json())
           .then(data =&gt; {
                const notificationBell = document.getElementById(&#39;notification-bell&#39;);

                const existingCount = document.getElementById(&#39;unread-bell-count&#39;);
                console.log(data.unreadCount);

                if (existingCount) {
                    notificationBell.removeChild(existingCount);
                }

                if (data.unreadCount &gt; 0) {
                    const unreadBellCount = document.createElement(&#39;span&#39;);
                    unreadBellCount.id = &#39;unread-bell-count&#39;;
                    unreadBellCount.className = &#39;badge&#39;;
                    unreadBellCount.textContent = data.unreadCount;

                    notificationBell.appendChild(unreadBellCount);
                }
           });
}
</code></pre><ul>
<li>페이지 접근 시, 읽지 않은 값을 테이블에서 가져와 출력<pre><code>notificationContainer.appendChild(notificationModal);
let unreadCountElement = document.getElementById(&#39;unread-bell-count&#39;);
if (!unreadCountElement) {
  unreadCountElement = document.createElement(&#39;span&#39;);
  unreadCountElement.id = &#39;unread-bell-count&#39;;
  unreadCountElement.className = &#39;badge&#39;;
  document.getElementById(&#39;notification-bell&#39;).appendChild(unreadCountElement);
}
// 현재 카운트 가져오기 및 증가
const currentCount = parseInt(unreadCountElement.textContent) || 0;
unreadCountElement.textContent = currentCount + 1;</code></pre></li>
<li>모달창과 동시에 현재 카운트에서 +1을 하여 값을 실시간 증가</li>
<li>이를 통해 알림 개수를 실시간, 기존 값을 노출 가능<pre><code>//통합 알림 드롭 리스트
notificationModal.innerHTML = `
  &lt;li id=&quot;mark-as-read&quot; class=&quot;mark-as-read&quot; style=&quot;font-size: 10px; text-align: right; color: gray;&quot;&gt;일괄읽음&lt;/li&gt;
`;
addMarkAsReadListener();
data.unreadList.forEach(notification =&gt; {
  const listItem = document.createElement(&#39;li&#39;);
  listItem.setAttribute(&#39;data-notification-no&#39;, notification.nofication_no);
  listItem.setAttribute(&#39;data-notification-type&#39;, notification.nofication_type);
  listItem.setAttribute(&#39;data-notification-type-pk&#39;, notification.nofication_type_pk);
  const date = new Date(notification.nofication_create_date);
  const formattedDate = date.toLocaleDateString(&#39;ko-KR&#39;, {
       year: &#39;2-digit&#39;,
       month: &#39;2-digit&#39;,
       day: &#39;2-digit&#39;
  });
  const formattedTime = date.toLocaleTimeString(&#39;ko-KR&#39;, {
       hour: &#39;2-digit&#39;,
       minute: &#39;2-digit&#39;,
       hour12: true
  });
  listItem.innerHTML = `
       &lt;strong style=&quot;margin-bottom: 5px;&quot;&gt;${notification.nofication_title}&lt;/strong&gt;
       &lt;p&gt;${notification.nofication_content}&lt;/p&gt;
       &lt;em style=&quot;display: block; margin-bottom: 5px; float: right;&quot;&gt;${formattedDate} ${formattedTime}&lt;/em&gt;
       &lt;hr style=&quot;border: none; margin: 10px 0;&quot;&gt;
  `;
</code></pre></li>
</ul>
<pre><code>- 클릭 모션과 동시에 드롭 리스트가 열리면서 해당 값들을 가져와 출력
- 드롭 리스트가 열려있는 상태에서 알림이 오게 되면, 이 또한 js에서 실시간 웹소켓 값을 통해 노출되어 반영되도록 구성함</code></pre><p> alarmSocket.onmessage = function(event) {
     const message = JSON.parse(event.data);
     const currentType = message.nofication_type;
     const notificationModal = document.getElementById(&#39;notification-bell-modal&#39;);
     if(message.pk != null){
         if(message.type === &#39;vacationApprovalAlarm&#39;){
           const title = message.title;
           const content = message.content;
           if (notificationModal.children.length &lt;= 1) {
              notificationModal.innerHTML = <code>&lt;li id=&quot;mark-as-read&quot; class=&quot;mark-as-read&quot; style=&quot;font-size: 10px; text-align: right; color: gray;&quot;&gt;일괄읽음&lt;/li&gt;</code>;
              addMarkAsReadListener();
           }
           message.data.forEach(function(item) {
               const listItem = document.createElement(&#39;li&#39;);
               listItem.setAttribute(&#39;data-notification-no&#39;, item.nofication_pk);
                        listItem.setAttribute(&#39;data-notification-type&#39;, message.nofication_type);
                        listItem.innerHTML = <code>&lt;strong style=&quot;margin-bottom: 5px;&quot;&gt;${title}&lt;/strong&gt;
                        &lt;p&gt;${content}&lt;/p&gt;
                        &lt;em style=&quot;display: block; margin-bottom: 5px; float: right;&quot;&gt;${message.timestamp}&lt;/em&gt;
                        &lt;hr style=&quot;border: none; margin: 10px 0;&quot;&gt;</code>;
                        notificationModal.insertBefore(listItem, notificationModal.children[1]);
                    });</p>
<pre><code>- 실시간 생성은 맨 위에 노출되어야 하기에 notificationModal.children[1]를 통해 맨 위에 출력
&lt;br&gt;

### 깨달은 점
- 실시간 반영에 신경써야 할 세부적인 요소들이 많아서 웹소켓에서 값을 받아 js에서 표현할 때 꼼꼼히 진행해야 함
- 기존 채팅 웹소켓을 통해 큰 오류없이 진행은 순조로웠음</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[그룹웨어 - 알림 구현(2)]]></title>
            <link>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%842</link>
            <guid>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%842</guid>
            <pubDate>Mon, 28 Oct 2024 06:33:50 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/chaedud_02/post/bf927e35-0b2d-4a29-bad7-822a7ed54767/image.png" alt=""></p>
<h3 id="구성">구성</h3>
<ul>
<li>이벤트가 발생과 동시에 사용자 브라우저 오른쪽 아래 모달 창을 통해 확인 가능해야 함</li>
<li>기능 이름과 동작 문구를 통해 알림을 구별하도록 구성해야 함</li>
<li>시간 출력을 통해 실시간 임을 확인 </li>
<li>5초 뒤에 자동으로 사라지도록 구성하고 여러 개가 올 경우, 화면에 쌓이는 모션을 추가<br>

</li>
</ul>
<h3 id="알림-구현-디비">알림 구현 디비</h3>
<p><img src="https://velog.velcdn.com/images/chaedud_02/post/57b1a564-4199-4d85-a363-39d4781711a8/image.png" alt=""></p>
<ul>
<li>그룹웨어의 다양한 기능들이 사용해야 하므로 기능 별 타입 값을 지정</li>
<li>알림을 받는 사람은 알림이 노출되어야 하는 사람의 pk 값</li>
<li>사원 번호는 이 알림을 보낸 사람의 pk 값</li>
<li>알림 노출 시간을 위한 알림 실시간 디비 시간</li>
<li>알림을 읽음 처리하고 개수를 차감하기 위한 알림 읽음 상태 값 구성</li>
<li>큰 구성은 이와 같지만 pk값이지만 실제 외래키는 많이 사용하지 않았음</li>
<li>기능 별로 사용되는 요청하는 사용자 번호가 다를 수 있어 입력하는 쪽으로 구성(전자 결재)<br>

</li>
</ul>
<h3 id="기능-별-사용-방법">기능 별 사용 방법</h3>
<pre><code>//기능 별 js
alarmSocket.send(JSON.stringify({
   type: &#39;nofication+본인 기능명&#39;,
   데이터이름 : 데이터
}));
// 값을 실시간으로 보내고 싶을때 사용
// 버튼 동작과 함께 동작되게 해주세요.
// ex) 메시지를 전송할때 함수 안에 추가로 집어넣어 실시간으로 알림소켓과 연결


//자신의 기능 페이지이 동작하는 js에 디비에 저장해뒀던 타입을 선언하기
document.addEventListener(&quot;DOMContentLoaded&quot;, function() {
//필수로 넣기
    window.functionType = 1; // 숫자는 꼭 디비의 본인 기능 숫자와 동일하게 해주세요.
    console.log(&quot;현재 기능 타입: &quot; +window.functionType);

    if (window.functionType === 1) {
        markNotificationsAsRead(window.functionType);
    }
});

//알림 모달(공통 js)
const alarmModal = document.getElementById(&quot;notification-modal&quot;);
const closeButton = document.querySelector(&quot;.close-notification-modal&quot;);

function showNotification(title, content, memberNo, time, type, pk) {

    if (memberNo === currentMember) {
        const notificationContainer = document.getElementById(&quot;notificationContainer&quot;);
        const notificationModal = document.createElement(&quot;div&quot;);

        notificationModal.classList.add(&quot;notification-modal&quot;);

        notificationModal.innerHTML = `
            &lt;div class=&quot;notification-modal-content&quot; data-notification-type=&quot;${type}&quot; data-notification-type-pk=&quot;${pk}&quot;&gt;
                &lt;strong&gt;${title}&lt;/strong&gt;
                &lt;p&gt;${content}&lt;/p&gt;
                &lt;input type=&quot;hidden&quot; name=&quot;memberNo&quot; value=&quot;${memberNo}&quot;&gt;
                &lt;span class=&quot;notification-time&quot;&gt;${time}&lt;/span&gt;
                &lt;span class=&quot;close-notification-modal&quot;&gt;&amp;times;&lt;/span&gt;
            &lt;/div&gt;
        `;

        notificationContainer.appendChild(notificationModal);

        setTimeout(() =&gt; {
            notificationModal.classList.remove(&quot;show&quot;);
            setTimeout(() =&gt; {
                notificationModal.remove();
            }, 400);
        }, 7000);

        notificationModal.querySelector(&#39;.close-notification-modal&#39;).addEventListener(&#39;click&#39;, function() {
            notificationModal.classList.remove(&quot;show&quot;);
            setTimeout(() =&gt; {
                notificationModal.remove();
            }, 400);
        });

        notificationModal.querySelector(&#39;.notification-modal-content&#39;).addEventListener(&#39;click&#39;, function() {
            const notificationType = this.getAttribute(&#39;data-notification-type&#39;);
            const notificationNo = this.getAttribute(&#39;data-notification-no&#39;);
            const notificationTypePk = this.getAttribute(&#39;data-notification-type-pk&#39;);
            if (noficationTypeUrl[notificationType]) {
                if (notificationTypePk === null || notificationTypePk === &#39;&#39;) {
                    window.location.href = noficationTypeUrl[notificationType];
                } else {
                    window.location.href = noficationTypeUrl[notificationType] + notificationTypePk;
                }
            } else {
                console.log(&#39;알 수 없는 알림 타입:&#39;, notificationType);
            }
        });
    }

}</code></pre><ul>
<li>위처럼 예시 코드를 통해 다른 팀원들이 자신의 기능 타입을 통해 알림 웹소켓에 값이 도달할 수 있도록 구성</li>
<li>도달 후, 테이블에 해당 값들을 입력하고 성공하여 다시 js에 값을 보냄</li>
<li>그 값을 통해 메세지를 받아야 하는 사용자의 번호와 현재 로그인하고 있는 사용자의 번호가 동일하면 모달창이 노출될 수 있도록 함<br>


</li>
</ul>
<h3 id="트러블-슈팅">트러블 슈팅</h3>
<ul>
<li>단순하게 페이지 진입하면 해당 알림을 볼 수 있고, 기능별 알림은 하나로 생각했으나, 전자 결재에서 문제 발생</li>
<li>전자 결재는 결재 순서에 따라 다음 사용자에게 알림이 가야하고, 알림을 읽지 않고 기능쪽 변화가 이루어지지 않는다면 그 다음 사람에게 알림이 가지 않아야 함</li>
<li>이를 고려하지 않고, 큰 기능 타입 번호를 구성하여 전자 결재 읽음 처리에도 문제 발생<br>

</li>
</ul>
<h3 id="해결">해결</h3>
<ul>
<li><p>전자 결재를 위한 전자결재 pk값을 디비에 넣어 구성하기로 결정</p>
</li>
<li><p>디비에 너무 많은 값들이 들어가고 복잡한 만큼 조인을 줄이기 위해 정규화를 하지 않음</p>
</li>
<li><p>전자 결재를 제외한 다른 기능들은 해당 pk값이 기본 null로 구성되어 이를 통해 조건을 나누기로 결정</p>
<pre><code>// 전자 결재(휴가 결재)
if (references != null &amp;&amp; !references.isEmpty()) {
  Long firstReference = references.get(0); 
  noficationDto.setNofication_content(nofication_content);
  noficationDto.setNofication_receive_no(firstReference);
  noficationDto.setNofication_title(nofication_title);
  noficationDto.setNofication_type(nofication_type);
  noficationDto.setMember_no(senderNo);
  noficationDto.setNofication_type_pk(vacationApprovalPk);//휴가 결재 pk 전달

  if (noficationService.insertAlarm(noficationDto) &gt; 0) {
      long notificationPk = noficationService.insertAlarmPk(); // pk값 추가
      Map&lt;String, Object&gt; memberUnreadCount = new HashMap&lt;&gt;();
      memberUnreadCount.put(&quot;memberNo&quot;, firstReference);
      memberUnreadCount.put(&quot;nofication_pk&quot;, notificationPk);//같이 넘길 값
      unreadCounts.add(memberUnreadCount);
  }
}</code></pre></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[그룹웨어 - 알림 구현(1)]]></title>
            <link>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%841</link>
            <guid>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%95%8C%EB%A6%BC-%EA%B5%AC%ED%98%841</guid>
            <pubDate>Mon, 28 Oct 2024 06:04:25 GMT</pubDate>
            <description><![CDATA[<p>통합 알림은 다른 기능도 다 사용해야 함
3일 안에 구현 완료 예정
메소드를 공유할 예정</p>
<h3 id="목표">목표</h3>
<p><img src="https://velog.velcdn.com/images/chaedud_02/post/60b0ebf9-5124-4d82-8124-630b9d86b2c8/image.png" alt=""></p>
<ul>
<li>통합 알림 구현을 위해 웹소켓 전용 구성</li>
<li>3일 안에 빠르게 구성 후, 다른 기능과 함께 테스트 예정<br>

</li>
</ul>
<h3 id="기능-구현-시작-전">기능 구현 시작 전</h3>
<ul>
<li>채팅 웹소켓과 알림 웹소켓이 충돌되지 않도록 구성</li>
<li>다른 기능들도 웹소켓 핸들러를 전용으로 만들어서 최대한 서로 영향없이 구성</li>
<li>제출까지 시간이 없는 관계로 처음부터 구성할 때 테스트를 많이 해봐야 함<br>

</li>
</ul>
<h3 id="기능-구현-시작">기능 구현 시작</h3>
<ul>
<li>채팅 웹소켓과 충돌이 될 수 있어 먼저 구현 후 설명</li>
<li>다른 팀원들이 사용하기에 어려움 없이 구성해야 함</li>
<li>핸들러로 구별해야 하고 기능을 타입별로 구성하여 원하는 페이지 접근 시, 알림 개수 차감 여부 확인 </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[그룹웨어 - 채팅 구현(8)]]></title>
            <link>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%B1%84%ED%8C%85%EA%B5%AC%ED%98%848</link>
            <guid>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%B1%84%ED%8C%85%EA%B5%AC%ED%98%848</guid>
            <pubDate>Tue, 22 Oct 2024 06:53:11 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/chaedud_02/post/03099532-1fbc-4042-8ec4-8a01baff742b/image.png" alt=""></p>
<h3 id="채팅방-나가기">채팅방 나가기</h3>
<ul>
<li>사용자 별 원하는 채팅방을 설정 목록 통해 나가기를 진행</li>
<li>개인 채팅방을 나갈 경우, 다른 사용자는 대화 내용 유지 </li>
<li>그룹 채팅방을 나갈 경우, 나감 메시지 출력과 동시에 대화 내용유지</li>
<li>개인은 나감과 동시에 재초대가 불가하고, 다시 생성</li>
<li>그룹은 나감과 동시에 재초대가 가능하고, 초대된 사용자는 그 전의 내용을 확인 불가능<br>

</li>
</ul>
<h3 id="구성">구성</h3>
<ul>
<li>채팅방을 나감과 동시에 채팅방 참여 상태를 변화</li>
<li>채팅방 목록은 이 상태를 확인 후, 사용자에게 출력</li>
<li>그룹 채팅방을 나간 후 재초대, 그 시점을 기준으로 채팅 메시지 값을 출력</li>
<li>나감과 동시에 다른 사용자들을 위해 출력 메시지를 디비에 저장<pre><code>&lt;!-- 채팅방 나가기   --&gt;
&lt;update id=&quot;chatRoomOut&quot;&gt;
 UPDATE fl_chat_member SET
 chat_member_par = 1
 WHERE  chat_room_no = #{chatRoomNo}
 AND member_no = #{memberNo}
&lt;/update&gt;</code></pre></li>
</ul>
<pre><code> &lt;!-- 채팅 내용 가져오기   --&gt;
&lt;select id=&quot;getChatMessages&quot; parameterType=&quot;java.util.Map&quot; resultType=&quot;java.util.HashMap&quot;&gt;
   SELECT
   cm.chat_message_create_date AS chatMessageCreateDate,
   cm.chat_content AS chatContent,
   m.member_name AS senderName,
   cm.chat_room_no AS roomNo,
   cm.chat_sender_no AS senderNo
   FROM fl_chat_message cm
   LEFT JOIN fl_member m ON cm.chat_sender_no = m.member_no
   JOIN fl_chat_member ctm ON cm.chat_room_no = ctm.chat_room_no
   WHERE ctm.member_no = #{memberNo}
   AND ctm.chat_member_par = 0
   AND cm.chat_message_create_date &gt;= ctm.chat_member_join_date
   AND (cm.chat_room_no = #{roomNo}  OR cm.chat_sender_no = 0 OR (cm.chat_room_no = ctm.chat_room_no AND cm.chat_content LIKE &#39;%초대%&#39;))
   AND cm.chat_room_no = #{roomNo}
   GROUP BY cm.chat_content, cm.chat_sender_no, cm.chat_message_create_date, m.member_name, cm.chat_room_no
   ORDER BY cm.chat_message_create_date ASC;
&lt;/select&gt;</code></pre><ul>
<li><p>채팅방 나가기 메시지 Handler를 통해 실시간으로 나간 사용자를 출력</p>
<pre><code>// 나가기 메시지(socket)
private void saveOutMessage(String sentence, Long currentChatRoomNo) {
  String inviteMessage = sentence+&quot;님이 나가셨습니다.&quot;;

  ChatMessageDto chatMessageDto = new ChatMessageDto();
  chatMessageDto.setChat_room_no(currentChatRoomNo);
  chatMessageDto.setChat_sender_no(0L);
  chatMessageDto.setChat_content(inviteMessage);
  chatMessageService.saveChatMessage(chatMessageDto);
}

</code></pre></li>
</ul>
<p>//나가기 실시간 반영(js)
else if(message.type === &quot;chatMemCount&quot;) {
    const count = message.data;
    const chatRoom = message.currentChatRoomNo;
    if(currentChatRoomNo === chatRoom){
      const participantCount = document.getElementById(&#39;participantCountSpan&#39;);
      participantCount.textContent = count;
      const messageElement = document.createElement(&#39;div&#39;);
      messageElement.classList.add(&quot;system-message&quot;, &quot;messageItem&quot;);
      messageElement.innerHTML = <code>&lt;div class=&quot;message-ele&quot;&gt;
         &lt;div class=&quot;system-content&quot;&gt;
         &lt;p&gt;${message.outSentence}님이 나가셨습니다.&lt;/p&gt;
         &lt;/div&gt;
         &lt;/div&gt;</code>;</p>
<pre><code>  const chatContainer = document.getElementById(&#39;chatContent&#39;);
  chatContainer.appendChild(messageElement);
  chatContainer.scrollTop = chatContainer.scrollHeight;
}</code></pre><p>}</p>
<pre><code>&lt;br&gt;

### 나가기 메시지 확인

![](https://velog.velcdn.com/images/chaedud_02/post/1b89d0f3-5707-4c06-955e-9390f0a88906/image.png)
- 같은 채팅방 참여자들에게 확인 가능한 메시지 출력
- 나감과 동시에 위에 사용자 수도 실시간 차감

&lt;br&gt;

### 구현하면서 들었던 생각
- 기본적으로 나감과 초대가 간단하게 상태값 변화만 진행한다고 생각
- 실시간 변화를 보여줘야 함을 넘어서 다른 사용자들에게도 변화를 보여줘야 한다는 점
- 생각했던 부분보다 더 자세한 조건이 들어가고 여러 방면에서 채팅방 접근할 수 있는 부분을 제한 걸어야 했음
- 제일 나중에 생각해서 고려했던 것이 채팅 메시지 출력
- 나가면 그 전의 내용을 확인 못하게 해야한다는 점</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[그룹웨어 - 채팅 구현(7)]]></title>
            <link>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%847</link>
            <guid>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%847</guid>
            <pubDate>Thu, 17 Oct 2024 08:33:34 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/chaedud_02/post/05422e75-e888-4104-8e2c-d95b7be4956b/image.png" alt=""></p>
<h3 id="채팅-안읽음-처리">채팅 안읽음 처리</h3>
<ul>
<li>채팅방 별 사용자가 읽지 않은 개수를 목록에 출력</li>
<li>전송한 사용자는 메시지에 대해 읽음 처리가 바로 가능해야 함</li>
<li>그룹 채팅방은 속한 사용자가 보낸 메시지 개수를 전부 확인 해야 함</li>
</ul>
<br>

<h3 id="채팅-안읽음-개수">채팅 안읽음 개수</h3>
<ul>
<li><p>사용자가 페이지에 진입과 동시에 채팅방 목록에 해당 안읽음 개수를 검색</p>
<pre><code>  &lt;!-- 채팅방 별 안읽은 개수--&gt;
  &lt;select id=&quot;getUnreadCounts&quot; parameterType=&quot;java.lang.Long&quot; resultType=&quot;java.util.Map&quot;&gt;
      SELECT COUNT(cm.chat_message_no) AS unread_count, cm.chat_room_no
      FROM fl_chat_message cm
      LEFT JOIN fl_chat_read_status cr ON cm.chat_message_no = cr.chat_message_no AND cr.member_no = #{memberNo}
      JOIN fl_chat_member ctm ON cm.chat_room_no = ctm.chat_room_no
      WHERE cr.chat_read_no IS NULL
      AND cm.chat_room_no IS NOT NULL
      AND ctm.member_no = #{memberNo}
      AND cm.chat_message_create_date &gt;= ctm.chat_member_join_date AND cm.chat_sender_no NOT IN (0)
      GROUP BY cm.chat_room_no;

  &lt;/select&gt;</code></pre></li>
<li><p>위의 쿼리문을 통해 현재 사용자의 번호를 가지고 채팅방 별 안읽은 개수를 모두 체크하여 서비스쪽으로 값을 넘김</p>
</li>
<li><p>Group By를 통해 같은 채팅방 번호끼리 개수를 확인 가능</p>
</li>
</ul>
<br>

<h3 id="실시간-반영">실시간 반영</h3>
<ul>
<li>페이지 진입 후, 실시간으로 보낸 메시지 개수 또한 증가 시켜야 함</li>
<li>같은 방 사용자가 메시지를 전송과 동시에 실시간으로 같은 방 사용자들에게 안읽음 개수를 증가시켜 일괄 목록에 표현하도록 구현<br>

</li>
</ul>
<h3 id="채팅-읽음-처리-디비">채팅 읽음 처리 디비</h3>
<pre><code>//채팅 읽음 디비
CREATE TABLE fl_chat_read_status (
    chat_read_no INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT &#39;읽음 상태 번호&#39;,
    chat_message_no INT UNSIGNED NOT NULL COMMENT &#39;메시지 번호&#39;,
    member_no INT UNSIGNED NOT NULL COMMENT &#39;사원 번호&#39;,
    chat_read_date DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT &#39;읽은 시간&#39;,
    CONSTRAINT chat_read_message_no_fk
    FOREIGN KEY (chat_message_no) 
    REFERENCES fl_chat_message(chat_message_no)
    ON DELETE CASCADE
    ON UPDATE CASCADE,
    CONSTRAINT chat_read_emp_no_fk
    FOREIGN KEY (member_no) 
    REFERENCES fl_member(member_no)
    ON DELETE CASCADE
    ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT &#39;읽음 상태&#39;;

//발신자 채팅 읽음 트리거

DELIMITER //

CREATE TRIGGER after_chat_message_insert
AFTER INSERT ON fl_chat_message
FOR EACH ROW
BEGIN
    IF NEW.chat_sender_no &lt;&gt; 0 THEN
        INSERT INTO fl_chat_read_status (chat_message_no, member_no, chat_read_date)
        VALUES (NEW.chat_message_no, NEW.chat_sender_no, CURRENT_TIMESTAMP);
    END IF;
END;
//
DELIMITER ;</code></pre><ul>
<li>메시지 전송과 동시에 해당 사용자의 번호와 메시지 번호를 가지고 읽음 테이블에 값을 입력</li>
<li>이를 통해 발신자는 읽음 처리가 동시에 가능<br>

</li>
</ul>
<h3 id="채팅방-진입-시-읽음-처리">채팅방 진입 시 읽음 처리</h3>
<ul>
<li><p>채팅방 진입 시, 읽지 않은 메시지를 일괄 읽음 처리하고 개수를 차감</p>
</li>
<li><p>채팅방 번호와 현재 사용자 번호, 메시지 번호를 지니고 채팅 읽음 테이블에 값을 INSERT하여 읽음 처리 진행</p>
<pre><code>  // 읽음 처리(js)
  function markAllMessagesAsRead(chatRoomNo) {
      const message = {
          type: &#39;markAsRead&#39;,
          chatRoomNo: chatRoomNo,
          currentMember: currentMember
      };

      socket.send(JSON.stringify(message));
  }

  ----------------------------------------------------------------------------------------------

  //채팅 읽음 처리(socket)
  private void handleChatRead(Map&lt;String, Object&gt; jsonMap, WebSocketSession session ,String type) throws Exception {
      Long currentMemberNo = Long.parseLong((String) jsonMap.get(&quot;currentMember&quot;));
      Long roomNo = ((Number) jsonMap.get(&quot;chatRoomNo&quot;)).longValue();
      List&lt;Long&gt; messageNo = chatMessageService.markMessagesAsReadForChatRoom(currentMemberNo, roomNo);
      if (!messageNo.isEmpty()) {
          for (Long i : messageNo) {
              ChatReadDto chatReadDto = new ChatReadDto();
              chatReadDto.setMember_no(currentMemberNo);
              chatReadDto.setChat_message_no(i);

              chatMessageService.insertReadStatus(chatReadDto);
         }

      }
      Map&lt;String, Object&gt; response = new HashMap&lt;&gt;();
      response.put(&quot;type&quot;, &quot;updateUnreadCount&quot;);
      response.put(&quot;chatRoomNo&quot;, roomNo);
     response.put(&quot;unreadCount&quot;, 0);

      session.sendMessage(new TextMessage(new ObjectMapper().writeValueAsString(response)));
  }
</code></pre></li>
</ul>
<p>```</p>
<ul>
<li>해당 채팅방 목록을 클릭과 동시에 실시간으로 웹소켓으로 해당 정보를 가지고 감</li>
<li>웹소켓 핸들러에서 읽음 처리 메소드로 이동</li>
<li>해당 메소드에서 안읽음 메시지를 모두 조회 </li>
<li>조회한 메시지 번호와 사용자 번호를 가지고 읽음 처리 디비에 값 INSERT<br>

</li>
</ul>
<h3 id="채팅방-이용-시-개수-증가-제한">채팅방 이용 시, 개수 증가 제한</h3>
<ul>
<li>현재 채팅방을 이용하고 , 실시간 소통 중이라면 바로바로 확인 처리를 구성</li>
<li>보냄과 동시에 사용자에게 해당 정보가 도착하고 그 채팅방 번호와 사용자가 이용 중인 채팅방 번호가 동일하면 디비에 읽음 처리 진행</li>
<li>전역 변수로 채팅방 번호를 가지고 있다가 동작이 변화하면 이를 판단하여 진행<br>

</li>
</ul>
<h3 id="트러블-슈팅">트러블 슈팅</h3>
<ul>
<li>발신자의 읽음 처리를 고민하지 않고, 디비를 설계했음</li>
<li>보낸 사용자도 읽음 개수가 실시간 반영된다는 점을 깨닫고 동시에 읽음 처리를 서비스에서 진행할까 고민</li>
<li>하지만 구조상 너무 비효율적인 작업이라 생각함</li>
<li>디비에 트리거를 추가해서 바로 읽음 처리될 수 있도록 구성해야 겠다고 생각</li>
<li>위처럼 추가 후, 발신자는 개수 증가를 제한 <br>

</li>
</ul>
<h3 id="깨달은-점">깨달은 점</h3>
<ul>
<li>실시간 반영과 디비 반영을 동시에 진행해야 하므로 어떤 순서로 구성해야 합리적일지 고민을 많이 함</li>
<li>어느 부분에선 실시간 반영을 먼저하고 디비에 값을 넣으면 값 반영에 대해 오류가 발생하는 경우가 생김</li>
<li>구성하는 부분과 설계순서에 대해 많이 고민할 필요가 있음</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[그룹웨어 - 채팅 구현(6)]]></title>
            <link>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%846</link>
            <guid>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%846</guid>
            <pubDate>Wed, 16 Oct 2024 06:57:35 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/chaedud_02/post/ff5c5444-d9cd-4675-9f7b-4aa392f218ef/image.png" alt=""></p>
<h3 id="목록-검색자동완성">목록 검색(자동완성)</h3>
<ul>
<li>UI에 표시된 채팅방 목록을 js를 통해 자동완성 기능 구현</li>
<li>js에서 같은 html 요소를 가지고 있는 목록의 방 이름을 찾음</li>
<li>배열에 저장 후, 검색어와 비교하여 목록이 출력되도록 구성</li>
<li>한 글자씩 작성과 동시에 비교하기 위해서 oninput 처리<pre><code>//검색 자동완성 기능
document.getElementById(&#39;searchInput&#39;).oninput = searchMem;
function searchMem() {
 let input = document.getElementById(&#39;searchInput&#39;).value.toLowerCase().replace(/\s+/g, &#39;&#39;);
 let chatItems = document.getElementsByClassName(&#39;chatItem&#39;);
 for (let i = 0; i &lt; chatItems.length; i++) {
    let chatName = chatItems[i].getElementsByTagName(&#39;p&#39;)[0].innerText.toLowerCase().replace(/\s+/g, &#39;&#39;);
    chatItems[i].style.display = chatName.includes(input) ? &quot;&quot; : &quot;none&quot;;
}
}</code></pre><br>

</li>
</ul>
<h3 id="상단-고정">상단 고정</h3>
<p><img src="https://velog.velcdn.com/images/chaedud_02/post/4812c508-fc1f-4839-8128-75df25554e85/image.png" alt=""></p>
<ul>
<li>원하는 채팅방을 상단 고정하기 위해 3가지를 고려</li>
<li>첫 번째, 리로드가 되어도 상단 고정이 되어야 함</li>
<li>두 번째, 실시간으로 생성되는 채팅방은 상단 고정 바로 아래 최신 순으로 정렬되어야 함</li>
<li>세 번째, 고정이 여러 개인 경우에 고정된 목록들끼리 최신 순 정렬이 가능해야 함<br>

</li>
</ul>
<h3 id="구성">구성</h3>
<ul>
<li>테이블에 채팅방 번호와 참여자 번호, 고정 상태와 고정된 시간을 구성</li>
<li>고정할 채팅방 클릭하여 채팅방 번호를 가지고 서버 서비스쪽으로 이동</li>
<li>현재 로그인한 사용자 번호와 채팅방 번호를 통해 고정 상태와 시간(update)을 UPDATE 수정함</li>
<li>채팅방 목록 SELECT문에서 고정 상태가 있을 경우, 고정 상태 최신으로 정렬함</li>
<li>고정상태가 여러 개이면, 최신순으로 정렬</li>
<li>그리고 현재 html에서 채팅방 목록 고정 pin이 노출된 경우에 그 마지막 요소 아래 생성되도록 구성</li>
</ul>
<pre><code>//채팅방 목록을 가지고 오는 쿼리문(고정 우선)
&lt;select id=&quot;selectChatList&quot; parameterType=&quot;java.lang.Long&quot; resultType=&quot;com.fiveLink.linkOffice.chat.domain.ChatMemberDto&quot;&gt;
    SELECT cm.*
    FROM fl_chat_member cm
    LEFT JOIN (
    SELECT cm.chat_room_no, MAX(cmsg.chat_message_create_date) AS recent_message_date
    FROM fl_chat_member cm
    JOIN fl_chat_message cmsg ON cm.chat_room_no = cmsg.chat_room_no
    WHERE cm.member_no = #{memberNo}
    GROUP BY cm.chat_room_no
    ) AS rm ON cm.chat_room_no = rm.chat_room_no
    JOIN fl_chat_room cr ON cm.chat_room_no = cr.chat_room_no
    WHERE cm.member_no = #{memberNo}
    AND cm.chat_member_par = 0
    ORDER BY
    cm.chat_member_pin DESC,
    CASE
    WHEN cm.chat_member_pin = 0 THEN
    COALESCE(rm.recent_message_date, cm.chat_member_join_date)
    END DESC,
    cm.chat_member_no DESC;
&lt;/select&gt;</code></pre><br>

<h3 id="트러블-슈팅">트러블 슈팅</h3>
<ul>
<li>채팅방 실시간 생성과 함께 html에 고정부분을 고려하여 목록을 정렬해야 하는 점에서 랜덤으로 생기는 오류 발생</li>
<li>js에서 div id=&quot;chatRooom&quot;을 실시간 생성하는 부분에서 오류 발생</li>
<li>디비에서 목록을 조회에서 가져오면 html 형식대로 가져오지만, 실시간 생성은 목록이 없을 경우에 그 형식을 지키기 않고 생성되어 랜덤 생성의 원인이 됨</li>
<li>html 구조 생성하는 방식을 실시간과 목록 출력과 동일하게 구성하여 오류 해결</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[그룹웨어 - 채팅 구현(5)]]></title>
            <link>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%845</link>
            <guid>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%845</guid>
            <pubDate>Sun, 13 Oct 2024 15:24:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/chaedud_02/post/ddafd504-0004-4a4d-b71a-002f8563f89c/image.png" alt=""></p>
<h3 id="그룹-채팅과-개인-채팅">그룹 채팅과 개인 채팅</h3>
<ul>
<li>그룹 채팅과 개인 채팅을 생성할 경우, 인원수에 따라 제한</li>
<li>예를 들어, 조직도에 선택된 사원이 2명 이상일 경우, 그룹방 명을 입력받아 디비에 타입을 구분하여 저장</li>
</ul>
<h3 id="구성">구성</h3>
<ul>
<li>조직도에서 선택된 값을 확인하여 채팅 타입을 정함</li>
<li>0은 개인, 1은 그룹으로 정함</li>
<li>목록 실시간 반영을 위해 웹소켓 핸들러에서 디비 저장과 반영을 진행</li>
<li>개인 채팅방은 상대방의 이름이 채팅방 이름이 되게 설정하고, 그룹 채팅방은 일괄 저장되기 때문에 같은 이름으로 설정함</li>
</ul>
<h3 id="개인-채팅방-생성">개인 채팅방 생성</h3>
<pre><code>//웹소켓에서 진행하는 채팅방 디비 저장
dto.setChat_room_type(0);//1:1 채팅방 타입
Long chatRoomNo = chatRoomService.createRoomOne(dto);
String position = chatRoomService.searchPosition(currentMemberNo);

//채팅방 이름을 위한 이름+부서명
String namePosition = currentMemberName + &quot; &quot; + position;

ChatMemberDto memberDto = new ChatMemberDto();
memberDto.setMember_no(Long.valueOf(members.get(0)));
memberDto.setChat_room_no(chatRoomNo);
memberDto.setChat_member_room_name(namePosition);
if(chatMemberService.createMemberRoomOne(memberDto)&gt;0){
    ChatMemberDto memberDto2 = new ChatMemberDto();
    memberDto2.setMember_no(currentMemberNo);
    memberDto2.setChat_room_no(chatRoomNo);
    memberDto2.setChat_member_room_name(names.get(0));
    if(chatMemberService.createMemberRoomOne(memberDto2)&gt;0){
        // 채팅방 번호로 해당 채팅방에 속한 멤버들 정보 조회
        List&lt;ChatMemberDto&gt; chatMembers = chatMemberService.getMembersByChatRoomNo(chatRoomNo);
        List&lt;Map&lt;String, Object&gt;&gt; memberInfoList = new ArrayList&lt;&gt;();
        for(ChatMemberDto member : chatMembers){
            Map&lt;String, Object&gt; memberInfo = new HashMap&lt;&gt;();
            memberInfo.put(&quot;memberNo&quot;, member.getMember_no());
            memberInfo.put(&quot;roomName&quot;, member.getChat_member_room_name());
            memberInfoList.add(memberInfo);
     }
     // 클라이언트로 보낼 데이터를 준비
     Map&lt;String, Object&gt; responseMap = new HashMap&lt;&gt;();
     responseMap.put(&quot;chatRoomNo&quot;, chatRoomNo);
     responseMap.put(&quot;currentMemberNo&quot;, currentMemberNo);
     responseMap.put(&quot;type&quot;, type);
     responseMap.put(&quot;memberInfoList&quot;, memberInfoList);
     // JSON으로 변환
     ObjectMapper objectMapper = new ObjectMapper();
     String responseJson = objectMapper.writeValueAsString(responseMap);
     for (WebSocketSession s : sessions.values()) {
         if (s.isOpen()) {
             s.sendMessage(new TextMessage(responseJson));
         }
    }
}
</code></pre><h3 id="그룹-채팅방-생성">그룹 채팅방 생성</h3>
<p><img src="https://velog.velcdn.com/images/chaedud_02/post/520ed20b-8d6a-4d41-925a-75bdef22b0aa/image.png" alt=""></p>
<pre><code>//그룹 채팅방 생성하기
dto.setChat_room_type(1);//단체 채팅방 타입
String groupChatName =(String) jsonMap.get(&quot;groupChatName&quot;);// 그룹 채팅명
dto.setChat_room_name(groupChatName);
Long chatRoomNo = chatRoomService.createRoomMany(dto);
for(int i = 0; i &lt; members.size(); i++){
    ChatMemberDto memberDto = new ChatMemberDto();
    memberDto.setChat_room_no(chatRoomNo);
    memberDto.setChat_member_room_name(groupChatName);
    memberDto.setMember_no(Long.valueOf(members.get(i)));
    // 멤버 추가
    chatMemberService.createMemberRoomMany(memberDto);

}
ChatMemberDto currentMemberDto = new ChatMemberDto();
currentMemberDto.setMember_no(currentMemberNo);
currentMemberDto.setChat_room_no(chatRoomNo);
currentMemberDto.setChat_member_room_name(groupChatName);
chatMemberService.createMemberRoomOne(currentMemberDto);

if (!members.contains(currentMemberNo)) {
    members.add(String.valueOf(currentMemberNo));
}

Map&lt;String, Object&gt; responseMap = new HashMap&lt;&gt;();
responseMap.put(&quot;chatRoomNo&quot;, chatRoomNo);
responseMap.put(&quot;members&quot;, members);
responseMap.put(&quot;type&quot;, &quot;groupChatCreate&quot;);
responseMap.put(&quot;names&quot;, groupChatName);

ObjectMapper objectMapper = new ObjectMapper();
String responseJson = objectMapper.writeValueAsString(responseMap);

for (WebSocketSession s : sessions.values()) {
    if (s.isOpen()) {
        s.sendMessage(new TextMessage(responseJson));
    }
}</code></pre><h3 id="구현하면서-들었던-생각">구현하면서 들었던 생각</h3>
<ul>
<li>채팅방 구분을 디비에서 타입 별로 구성하긴 했지만, 이 방법이 맞는건지 궁금</li>
<li>실시간 목록 반영을 위해 먼저 핸들러에서 디비 값을 저장하고 저장 완료될 경우, 값을 다시 화면에 실시간 출력되게 구성했음</li>
<li>이로 인해 너무 긴 코드와 같은 쿼리문을 가져다 쓰는 반복적인 행위가 계속 된다고 생각</li>
<li>어떻게 해야 조금 더 간략하고 가독성 좋은 코드로 만들지 고민 중</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[그룹웨어 - 채팅 구현(4)]]></title>
            <link>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%844</link>
            <guid>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%844</guid>
            <pubDate>Sat, 28 Sep 2024 16:10:15 GMT</pubDate>
            <description><![CDATA[<h3 id="채팅방-생성">채팅방 생성</h3>
<p><img src="https://velog.velcdn.com/images/chaedud_02/post/920750cb-b0f9-4329-85f1-9889e88f9561/image.png" alt=""></p>
<ul>
<li>조직도를 통해 채팅방 생성을 위해 member 값을 가지고 오기 위한 작업</li>
<li>선택한 사원의 번호를 배열형태로 담아 이동(그룹채팅방을 위해 배열로 진행)</li>
</ul>
<h3 id="트러블">트러블</h3>
<ul>
<li>확인을 누르고 채팅방 생성 Swal창을 통해 확인하려 했으나 웹소켓이 재실행되면서 동작을 막음</li>
<li>디비에는 저장이 되어, 채팅방 리로드할 경우에만 채팅방 목록이 업데이트 됨.</li>
<li>실시간 반영도 안되고 생겼는지 확인하지 못하는 문제 발생.</li>
<li>이유 찾는 기간만 3일 ....</li>
<li>Redis, pub/sub 기능을 고려하여 채팅 기능을 다시 구현하려 했으나 시간 부족과 다른 기능들로 인해 서버 구동이 처음부터 되지 않아 포기</li>
</ul>
<h3 id="해결">해결</h3>
<ul>
<li>js에서 확인 버튼클릭, form 제출과 동시에 url에 경로 변화가 생겨 웹소켓 재설정 오류 발생</li>
<li>form이나 확인 버튼과 같이 값을 넣어줘야 하는 상황에서, 아래와 같은 코드 추가<pre><code>//js에 경로 변경을 막아주는 기능
event.preventDefault();</code></pre></li>
<li>url변경을 제한하여 웹소켓 재설정을 막아 실시간 디비에 값이 들어가고, 화면에 채팅방 출력 가능</li>
</ul>
<p><img src="https://velog.velcdn.com/images/chaedud_02/post/c98f2cb5-d8cd-46f1-9aff-62de15289050/image.png" alt=""></p>
<ul>
<li>이런식으로 각자 채팅방 목록 출력하고 첫 채팅방 선택하여 실시간 채팅 확인</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[그룹웨어 - 채팅 구현(3)]]></title>
            <link>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%843</link>
            <guid>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%843</guid>
            <pubDate>Sat, 28 Sep 2024 15:54:11 GMT</pubDate>
            <description><![CDATA[<h3 id="실시간-11-채팅">실시간 1:1 채팅</h3>
<ul>
<li>웹소켓 연결과 동시에 모든 사용자에게 권한을 줌</li>
<li>이름, 내용, 날짜를 출력</li>
<li>하지만 실시간으로 날짜가 화면에 출력이 되지 않는 문제가 발생</li>
<li>확인은 리로드를 시켜서 값으로 확인</li>
</ul>
<p><img src="https://velog.velcdn.com/images/chaedud_02/post/6a06a436-8e7b-4466-b66f-9f2b7d2d8172/image.png" alt=""></p>
<h3 id="트러블">트러블</h3>
<ul>
<li>위의 설명처럼, 리로드를 시킨다는 점은 디비에 저장된 값을 불러와서 화면에 출력한다는 점이다.</li>
<li>메시지를 디비에 넣었을때 자동으로 디비에는 날짜가 입력되지만, 실시간 데이터에는 그 값을 가지고 와 화면에 출력할 수 없었음.</li>
<li>실시간으로 soket.onmessage를 통해서 js를 통해 화면에 요소를 추가하여 보여주었음</li>
<li>어떻게 해야 실시간으로 바로 날짜 정보를 찍을 수 있을지 고민함.</li>
</ul>
<h3 id="해결">해결</h3>
<ul>
<li>채팅 핸들러에서 디비에 값을 넣음과 동시에, 해당 메시지의 pk값과 정보를 select해서 정보를 저장</li>
<li>socket.onmessage를 통해 그 정보를 받아 요소마다 정보를 출력</li>
<li>이를 통해 값이 잘 노출되고, 리로드 시켜도 형식 유지 가능</li>
</ul>
<br>

<h4 id="아래와-같이-해당-정보들을-소켓으로-실시간-받아-표현">아래와 같이 해당 정보들을 소켓으로 실시간 받아 표현</h4>
<pre><code>const messageElement = document.createElement(&quot;div&quot;);
const now = new Date();
const formattedTime = formatDateTime(now);
const memberNo = parseInt(document.getElementById(&quot;currentMember&quot;).value, 10);
const memberNoCheck = parseInt(message.chat_sender_no, 10);
if (memberNoCheck === memberNo) {
    messageElement.classList.add(&quot;my-message&quot;, &quot;messageItem&quot;);
    messageElement.innerHTML = `
        &lt;div class=&quot;message-ele&quot;&gt;
          &lt;span class=&quot;message-time&quot;&gt;${formattedTime}&lt;/span&gt;
          &lt;div class=&quot;message-content&quot;&gt;
              &lt;p&gt;${message.chat_content}&lt;/p&gt;
          &lt;/div&gt;
        &lt;/div&gt;
   `;
} else {
        messageElement.classList.add(&quot;other-message&quot;, &quot;messageItem&quot;);
        messageElement.innerHTML = `
            &lt;div class=&quot;message-sender&quot;&gt;
                &lt;strong&gt;${message.chat_sender_name}&lt;/strong&gt;
            &lt;/div&gt;
            &lt;div class=&quot;message-ele&quot;&gt;
                 &lt;div class=&quot;message-content&quot;&gt;
                      &lt;p&gt;${message.chat_content}&lt;/p&gt;
                 &lt;/div&gt;
                 &lt;span class=&quot;message-time&quot;&gt;${formattedTime}&lt;/span&gt;
          &lt;/div&gt;
       `;
}</code></pre><h3 id="수정-후">수정 후</h3>
<p><img src="https://velog.velcdn.com/images/chaedud_02/post/82d138db-951a-4e0e-9da4-d1cc50b71935/image.png" alt=""></p>
<ul>
<li>디자인 적용 후, div 분리시켜 기본 틀 구성</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[관계형 & 비관계형 ]]></title>
            <link>https://velog.io/@chaedud_02/%EA%B4%80%EA%B3%84%ED%98%95-%EB%B9%84%EA%B4%80%EA%B3%84%ED%98%95</link>
            <guid>https://velog.io/@chaedud_02/%EA%B4%80%EA%B3%84%ED%98%95-%EB%B9%84%EA%B4%80%EA%B3%84%ED%98%95</guid>
            <pubDate>Sun, 15 Sep 2024 15:29:56 GMT</pubDate>
            <description><![CDATA[<h3 id="데이터베이스">데이터베이스</h3>
<ul>
<li>데이터를 체계적으로 저장하고 관리하는 시스템</li>
<li>대량의 데이터를 효율적으로 처리하고, 빠르고 쉽게 필요한 정보를 찾고 사용할 수 있도록 설계된 구조</li>
<li>관계형, 비관계형, 분산형<h4 id="dbms">DBMS</h4>
</li>
<li>데이터베이스 관리 시스템</li>
<li>데이터를 저장, 관리, 검색, 업데이트, 삭제하는 작업(CRUD)을 수행하는 소프트웨어<br>


</li>
</ul>
<h3 id="관계형-데이터베이스">관계형 데이터베이스</h3>
<ul>
<li>테이블 형식으로 저장하고 관리하는 데이터베이스 시스템</li>
<li>행과 열로 구성되며, 각각의 행은 개별 데이터 항목을 나타내고, 열은 그 데이터 항목의 속성이나 필드를 나타냄</li>
<li>MySQL, OracleDB, PostgreSQL...<h4 id="특징">특징</h4>
</li>
<li>데이터 관계 표현 : 서로 다른 테이블 간의 관계를 정의하여 테이블 간 데이터를 연결하고 참조 가능</li>
<li>SQL : 데이터를 조회, 삽입, 업데이트, 삭제 작업을 수행</li>
<li>정규화 : 중복 데이터를 최소화하고, 데이터 무결성을 유지하기 위해 정규화 과정을 거치며 테이블을 더 작은 테이블로 나누며, 테이블 간의 관계를 명확하게 정의<br>


</li>
</ul>
<h3 id="비관계형-데이터베이스nosql">비관계형 데이터베이스(NoSQL)</h3>
<ul>
<li>테이블 형식이 아닌 다양한 데이터 모델을 사용하여 데이터를 저장하고 관리하는 데이터 베이스</li>
<li>대규모의 데이터를 빠르게 처리하고, 유연한 데이터 구조를 허용</li>
<li>MongoDB, CouchDB, Redis, DynamoDB...</li>
</ul>
<h4 id="특징-1">특징</h4>
<ul>
<li><p>유연한 데이터 구조 : 데이터 스키마를 미리 정의할 필요가 없으며, 데이터 구조가 변경될 수 있으며, 이로 인해 비정형과 반정형 데이터 처리에 적합</p>
<blockquote>
<p>비정형 데이터 : 고정된 구조나 형식이 없는 데이터로, 체계적으로 정리되지 않거나 특정 스키마가 없는 데이터를 의미
반정형 데이터 : 어느 정도의 구조를 갖추고 있지만, 고정된 스키마가 없는 데이터로, 내부에 특정한 구조(태그, key-value)를 포함하고 있지만 엄격하지 않으며 유연성이 있음</p>
</blockquote>
</li>
<li><p>수평적 확장성 : 데이터를 여러 서버에 분산하여 저장할 수 있으며, 대용량 데이터를 처리할 때 관계형 데이터베이스보다 유연하게 확장이 가능</p>
</li>
<li><p>관계의 유연성 : 관계형 데이터베이스보다 테이블 간의 명시적인 관계가 필요하지 않으며, 다양한 데이터 모델을 사용하여 데이터 저장</p>
</li>
</ul>
<h4 id="단점">단점</h4>
<ul>
<li>데이터 일관성 문제</li>
<li>관계 표현의 복잡성</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[그룹웨어 - 채팅 구현(2)]]></title>
            <link>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%B1%84%ED%8C%852</link>
            <guid>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%B1%84%ED%8C%852</guid>
            <pubDate>Sun, 15 Sep 2024 13:52:59 GMT</pubDate>
            <description><![CDATA[<p>웹소켓 연결 유지 확인 </p>
<h3 id="웹소켓">웹소켓</h3>
<ul>
<li>클라이언트와 서버 간에 양방향 통신을 실시간으로 가능하게 하는 통신 프로토콜</li>
<li>서버와 클라이언트 간의 지속적인 연결을 유지하여 데이터를 실시간으로 주고받을 수 있게 함</li>
<li>HTTP와 달리 클라이언트가 요청을 보내지 않아도 서버가 데이터를 전송<h4 id="특징">특징</h4>
</li>
<li>실시간 양방향 통신: 양방향으로 데이터를 주고 받음</li>
<li>지속적인 연결 유지: 한번 연결이 이루어지면 HTTP와 달리 요청과 응답주기 없이 지속적인 데이터 통신이 가능</li>
<li>낮은 오버헤드 : HTTP처럼 매번 헤더를 주고받지 않아도 되어, 네트워크 트래픽이 줄어들어 오버헤드가 적음<blockquote>
<p><strong>오버헤드</strong> : 시스템 자원을 사용하거나 데이터를 처리할 때 추가로 발생하는 비용 또는 부하를 의미 </p>
</blockquote>
</li>
<li><em>HTTP오버헤드*</em> : HTTP는 요청마다 헤더, 쿠키, 인증 정보 등을 포함해 전송하는데 데이터의 크기가 작을수록 헤더의 비율이 상대적으로 커져 오버헤드가 커짐</li>
<li><em>WebSocket오버헤드*</em> : 웹소켓은 HTTP와 비교할 때 초기 연결을 유지하면서 추가적인 요청이나 응답없이 데이터를 주고 받으니 헤더를 반복해서 보내는 오버헤드가 적은 편임</li>
</ul>
<h3 id="웹소켓-연결">웹소켓 연결</h3>
<p><img src="https://velog.velcdn.com/images/chaedud_02/post/741d68f3-ddc4-40c5-8a22-212c038e50a9/image.png" alt="">
(1) 스프링부트 의존성 추가</p>
<pre><code>//Gradle
dependencies {
    implementation &#39;org.springframework.boot:spring-boot-starter-websocket&#39;
}</code></pre><p>(2) WebSocketConfig 설정</p>
<pre><code>
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    private final WebSocketHandler webSocketHandler;

    public WebSocketConfig(WebSocketHandler webSocketHandler) {
        this.webSocketHandler = webSocketHandler;
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // WebSocket 핸들러를 등록하고 엔드포인트 설정
        registry.addHandler(webSocketHandler, &quot;/ws&quot;).setAllowedOrigins(&quot;*&quot;);
    }
}
</code></pre><p>(3) WebSocketHandler 설정</p>
<pre><code>@Component
public class MyWebSocketHandler extends TextWebSocketHandler {

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 수신한 메시지 출력
        System.out.println(&quot;Received message: &quot; + message.getPayload());

        // 클라이언트에게 응답 메시지 전송
        session.sendMessage(new TextMessage(&quot;Server received: &quot; + message.getPayload()));
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        System.out.println(&quot;Client connected: &quot; + session.getId());
    }
}</code></pre><p>(4) 테스트한 값 확인하기
<img src="https://velog.velcdn.com/images/chaedud_02/post/c59f0785-b08e-41fb-8491-14c23f0de1a4/image.png" alt="">
<img src="https://velog.velcdn.com/images/chaedud_02/post/87d5ff39-86fc-49f1-b357-3c2948c8d202/image.png" alt=""></p>
<ul>
<li>인코딩 문제로 콘솔창에 한글 깨짐 발생은 있지만, 값 확인 가능</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[그룹웨어 - 채팅 구현(1)]]></title>
            <link>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%841</link>
            <guid>https://velog.io/@chaedud_02/%EA%B7%B8%EB%A3%B9%EC%9B%A8%EC%96%B4-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%841</guid>
            <pubDate>Sun, 15 Sep 2024 13:36:10 GMT</pubDate>
            <description><![CDATA[<p>그룹웨어 채팅을 구현시작 (09/09)
2주동안 진행할 예정</p>
<h3 id="목표">목표</h3>
<p><img src="https://velog.velcdn.com/images/chaedud_02/post/11c21573-a313-4761-bbe8-a91332285b35/image.png" alt=""></p>
<ul>
<li>위의 사진처럼 구성하는 것이 목표!</li>
<li>비동기로 구성 예정</li>
<li>실시간으로 각자 원하는 부분 동작</li>
<li>개인 채팅방(1:1), 그룹 채팅방까지 구현</li>
<li>통합알림까지 구현<br>


</li>
</ul>
<h3 id="프로젝트-시작-전">프로젝트 시작 전</h3>
<ul>
<li>시작 전에 제일 걱정되었던 것은 처음 사용해보는 웹소켓과 비동기로 화면이 자유롭게 동작할 수 있게 구성하는 점</li>
<li>먼저, 웹소켓에 대해 공부하기 시작했고 알고 시작하기 좋은 개념부터 공부하기 시작</li>
<li>pub/sub, redis, websocket, stomp 등과 같은 다양한 개념을 공부<br>


</li>
</ul>
<h3 id="프로젝트-시작">프로젝트 시작</h3>
<ul>
<li>가장 먼저 구성하고자 했던 부분은 DB</li>
<li>DB에서 먼저 채팅방, 채팅방 참여자, 메시지 저장할 수 있는 테이블 구성(순서대로)
<img src="https://velog.velcdn.com/images/chaedud_02/post/b336be7d-2c0b-42d4-938d-07b1920e040b/image.png" alt="">
<img src="https://velog.velcdn.com/images/chaedud_02/post/37d693a5-247a-4b4f-af45-d5fe5a70c35d/image.png" alt="">
<img src="https://velog.velcdn.com/images/chaedud_02/post/d6f3704b-1e4d-4d39-b3f4-5641f9c4e7eb/image.png" alt=""></li>
<li>테이블 구성을 통해 크게 흐름을 짐작</li>
<li>제일 까다롭다고 생각한 읽음 처리나 알림과 같이 연관이 되어있는 것들을 먼저 쿼리문을 통해 테스트 진행 후 시작<br>

</li>
</ul>
<h3 id="구성하면서-초기에-부딪힌-문제">구성하면서 초기에 부딪힌 문제</h3>
<ul>
<li>발신자, 수신자에 대한 명확한 기준이 필요했음</li>
<li>알림까지 고민하다보니 실시간 웹소켓을 어떻게 구성해서 표현해야 하는지 감이 안잡혔음</li>
<li>읽음 처리와 읽은 개수를 처리하기 위해 트리거로 실시간 바로 차감되게 구성하려니 메시지 보낸 사람 또한 읽은 개수로 포함해야하는 점으로 구성해야 했음</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[STOMP]]></title>
            <link>https://velog.io/@chaedud_02/STOMP</link>
            <guid>https://velog.io/@chaedud_02/STOMP</guid>
            <pubDate>Thu, 22 Aug 2024 06:59:35 GMT</pubDate>
            <description><![CDATA[<h3 id="stompsimple-text-oriented-messaging-protocol">STOMP(Simple Text Oriented Messaging Protocol)</h3>
<ul>
<li>메시지 브로커와 클라이언트 간의 통신을 위해 설계된 프로토콜</li>
<li>TCP 또는 WebSocket 같은 양방향 네트워크 프로토콜 기반으로 동작</li>
<li>주로 메시징 시스템에서 사용되며, 텍스트 기반의 프로토콜로서 사람이 읽기 쉽게 설계</li>
<li>WebSocket과 함께 사용되며 브라우저와 서버 간의 실시간 메시지 교환이 가능</li>
</ul>
<h4 id="특징">특징</h4>
<ul>
<li>텍스트 기반 프로토콜 : 모든 메시지는 텍스트로 표현되어 있으며, 이해하고 디버깅하기 쉽다.</li>
<li>독립적 플랫폼 : 다양한 언어와 플랫폼에서 구현이 가능하며, 클라이언트와 서버 사이의 통신을 단순하게 처리</li>
<li>메시징 시스템과의 호환성 : Apache ActiveMQ, RabbitMQ, JBoss Messaging과 같은 여러 메시징 시스템과 호환</li>
<li>프레임 구조 : &#39;프레임&#39;이라는 구조로 전송됨(명령, 헤더, 바디)<br>
###  메시지 브로커</li>
<li>분산 시스템 간에 데이터를 교환하기 위한 소프트웨어 </li>
<li>다양한 애플리케이션 간에 메시지를 송수신하고, 이를 관리하며 전송 중의 안정성을 보장하는 중요한 역할을 한다.<h4 id="유형">유형</h4>
</li>
<li>큐 기반 : 메시지가 큐에 저장되고, 수신자가 이를 소비하는 형태</li>
<li>발송(pub)/구독(sub) 기반 : 메시지를 특정 주제로 게시하고, 해당 주제를 구독한 모든 수신자가 메시지를 받는 형태<br>
### 연결 흐름</li>
<li>클라이언트 연결</li>
<li>메시지 전송</li>
<li>메시지 처리</li>
<li>메시지 브로드캐스트</li>
<li>클라이언트 수신</li>
</ul>
<pre><code>   클라이언트 A            서버(WebSocket + STOMP)             클라이언트 B
   ────────────             ────────────────────             ────────────
   1. 연결 요청  ──────────&gt;   2. 엔드포인트 연결          ────────────&gt; 3. 주제 구독
   4. 메시지 전송 ───────────&gt; 5. 메시지 처리 및 브로드캐스트 &lt;───────────&gt; 6. 메시지 수신
</code></pre><br>

<h3 id="springboot-websocket-stomp-의존성">SpringBoot WebSocket STOMP 의존성</h3>
<pre><code>//웹소켓을 사용하기 위한 라이브러리

dependecies {
    implementation &#39;org.springframework.boot:spring-boot-starter-websocket&#39;
}</code></pre><h4 id="참고자료">참고자료</h4>
<p><a href="https://velog.io/@hiy7030/chatting-1">https://velog.io/@hiy7030/chatting-1</a>
<a href="https://velog.io/@murphytklee/Spring-WebSocket-STOMP">https://velog.io/@murphytklee/Spring-WebSocket-STOMP</a>
<a href="https://velog.io/@ktf1686/Spring-WebSocket%EC%9C%BC%EB%A1%9C-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-STOMP%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%B1%84%ED%8C%85-%EA%B3%A0%EB%8F%84%ED%99%94">https://velog.io/@ktf1686/Spring-WebSocket%EC%9C%BC%EB%A1%9C-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-STOMP%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%B1%84%ED%8C%85-%EA%B3%A0%EB%8F%84%ED%99%94</a></p>
]]></description>
        </item>
    </channel>
</rss>