diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..0e13f0c2e8 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,8 @@ +#### Summary + +- PR의 내용을 요약해주세요. + + +#### Description + +- 본문에 무엇을 변경하였는지 자세히 적어주세요. \ No newline at end of file diff --git a/.github/workflows/backend-cd.yml b/.github/workflows/backend-cd.yml new file mode 100644 index 0000000000..18771e2dbf --- /dev/null +++ b/.github/workflows/backend-cd.yml @@ -0,0 +1,65 @@ +name: deploy to amazon ecs + +on: + push: + branches: [ main ] + paths: + - "backend/**" + +env: + AWS_REGION: ap-northeast-2 + ECR_URL: ${{ secrets.ECR_URL }} + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.SUBMODULE_TOKEN }} + submodules: true + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: 'gradle' + + - name: Get git commit rev + id: vars + run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + + - name: Grant execute permission for gradlew + working-directory: ./backend + run: chmod +x ./gradlew + + - name: Build Jar + working-directory: ./backend + run: ./gradlew bootJar -Dspring.profiles.active=prod + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build Docker Image & Push + uses: docker/build-push-action@v5 + with: + context: ./backend + file: ./backend/Dockerfile + push: true + tags: ${{ env.ECR_URL }}:latest + provenance: false + + - name: Deploy to Amazon ECS + run: | + aws ecs update-service --cluster ${{ secrets.CLUSTER }} --service ${{ secrets.SERVICE }} --force-new-deployment \ + --region ${{ env.AWS_REGION }} \ No newline at end of file diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 0000000000..1f81f7e2be --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,30 @@ +name : ci + +on: + pull_request: + paths: + - "backend/**" +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.SUBMODULE_TOKEN }} + submodules: true + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: 'gradle' + + - name: Grant execute permission for gradlew + working-directory: ./backend + run: chmod +x ./gradlew + + - name: Test + working-directory: ./backend + run: ./gradlew clean test \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..fc27970bcc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "backend/dclass-secret"] + path = backend/dclass-secret + url = https://github.com/devbelly/dclass-secret.git diff --git a/README.md b/README.md index b0c42847c6..a25dc9d7af 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,140 @@ -# Welcome to GitHub +
+ +

디클

-캡스톤 팀 생성을 축하합니다. +| Play Store | App Store | +| :----------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------: | +| | | -## 팀소개 및 페이지를 꾸며주세요. +![image](https://github.com/kookmin-sw/capstone-2024-07/assets/67682840/07265690-0986-49e2-b73f-06e9b606811f) -- 프로젝트 소개 - - 프로젝트 설치방법 및 데모, 사용방법, 프리뷰등을 readme.md에 작성. - - Api나 사용방법등 내용이 많을경우 wiki에 꾸미고 링크 추가. +
-- 팀페이지 꾸미기 - - 프로젝트 소개 및 팀원 소개 - - index.md 예시보고 수정. +### 1. 프로젝트 소개 +- 디클(Department class)은 전국의 대학생들이 학과를 중심으로 모여 소통할 수 있는 학과별 커뮤니티 서비스입니다. 모든 유저가 자신의 소속 학교나 동아리가 아닌 학과별로 자유롭게 모여 같은 학과끼리만 이해할 수 있는 깊은 고민과 전공 관련 정보를 공유할 수 있게 돕고자 합니다. -- GitHub Pages 리파지토리 Settings > Options > GitHub Pages - - Source를 marster branch - - Theme Chooser에서 태마선택 - - 수정후 팀페이지 확인하여 점검. +### 2. 소개 영상 +[![소개 영상](http://img.youtube.com/vi/f8lyoE0JIKA/0.jpg)](https://youtu.be/f8lyoE0JIKA?si=rLR_N2X6oFqUPunp) -**팀페이지 주소** -> https://kookmin-sw.github.io/ '{{자신의 리파지토리 아이디}}' +### 3. 팀 소개 -**예시)** 2023년 0조 https://kookmin-sw.github.io/capstone-2023-00/ +| Frontend | Frontend | Backend | Backend | +| :-------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------: | +| 권지아 | 윤홍현 | 윤웅배 | 김동윤 | +| [권지아(팀장)](https://github.com/jia5232/) | [윤홍현](https://github.com/hongbuly) | [윤웅배](https://github.com/devbelly) | [김동윤](https://github.com/zkxmdkdltm) | +``` +✨ Name : 권지아 +👩‍🎓 Student ID : 20190155 +📌 Role: 팀장, 기획, 프론트엔드 +``` -## 내용에 아래와 같은 내용들을 추가하세요. +``` +✨ Name : 윤홍현 +👩‍🎓 Student ID : 20213032 +📌 Role: UI, 프론트엔드 +``` -### 1. 프로잭트 소개 +``` +✨ Name : 윤웅배 +👩‍🎓 Student ID : 20171659 +📌 Role: 백엔드, 인프라 +``` -프로젝트 +``` +✨ Name : 김동윤 +👩‍🎓 Student ID : 20212674 +📌 Role: 백엔드, 인프라 +``` -### 2. 소개 영상 +### 4. 기술 스택 -프로젝트 소개하는 영상을 추가하세요 +![image](https://github.com/kookmin-sw/capstone-2024-07/assets/67682840/b9be449b-ddfe-44b8-99c6-93c1554ae7d3) -### 3. 팀 소개 -팀을 소개하세요. +### 5. 서비스 구조도 -팀원정보 및 담당이나 사진 및 SNS를 이용하여 소개하세요. +![image](https://github.com/kookmin-sw/capstone-2024-07/assets/67682840/893f626b-ab39-46c2-bd12-a04ec68bbe69) -### 4. 사용법 -소스코드제출시 설치법이나 사용법을 작성하세요. +### 6. 사용법 -### 5. 기타 +#### Backend -추가적인 내용은 자유롭게 작성하세요. +- Prerequisite + - Java 17 + - docker compose -## Markdown을 사용하여 내용꾸미기 +- 로컬 MYSQL 설치하기(M1 기준) + - 백엔드 파일 경로로 진입 -Markdown은 작문을 스타일링하기위한 가볍고 사용하기 쉬운 구문입니다. 여기에는 다음을위한 규칙이 포함됩니다. + ``` + cd backend + ``` + - `docker-compose`를 데몬으로 실행 + ``` + docker-compose up -d + ``` + - `backend/src/main/resources/application.yml` 포트 수정 + ```yml + spring: + datasource: + url: jdbc:mysql://localhost:{HOST_PORT}/dclass?serverTimezone=UTC + ``` -```markdown -Syntax highlighted code block +- AWS 설정하기 + - `backend/src/main/resources`에 `application-security.yml` 파일 생성 후 아래 내용 작성 -# Header 1 -## Header 2 -### Header 3 + ```yml + aws: + access-key: + secret-key: + + s3: + bucket: + region: "ap-northeast-2" + ``` -- Bulleted -- List +- 로컬 실행하기 + - `backend`에서 아래 명령어 실행 -1. Numbered -2. List + ``` + ./gradlew bootRun —args='—spring.profiles.active=local' + ``` -**Bold** and _Italic_ and `Code` text -[Link](url) and ![Image](src) -``` +#### Frontend + +- Prerequisite + - [Flutter 3.13.0](https://docs.flutter.dev/get-started/install) + - [Dart 3.1.0](https://dart.dev/get-dart) + - [안드로이드 스튜디오](https://developer.android.com/codelabs/basic-android-kotlin-compose-install-android-studio?hl=ko#0) + +- 에뮬레이터 (혹은 시뮬레이터) 실행 + - 안드로이드 스튜디오에서 device manager → virtual → create device → 실행 + +- 로컬 실행하기 + - 프론트엔드 파일 경로로 진입 + + ``` + cd frontend + ``` + - 패키지 설치 -자세한 내용은 [GitHub Flavored Markdown](https://guides.github.com/features/mastering-markdown/). + ``` + flutter pub get + ``` + - 프로젝트 실행 -### Support or Contact + ``` + flutter run + ``` +### 7. Document -readme 파일 생성에 추가적인 도움이 필요하면 [도움말](https://help.github.com/articles/about-readmes/) 이나 [contact support](https://github.com/contact) 을 이용하세요. +- [중간 보고서](https://github.com/kookmin-sw/capstone-2024-07/files/15328640/default.pdf) +- [중간 발표자료](https://github.com/kookmin-sw/capstone-2024-07/files/15328685/default.pdf) +- [최종 포스터](https://github.com/kookmin-sw/capstone-2024-07/files/15368233/default.pdf) +- [최종 발표자료](https://github.com/kookmin-sw/capstone-2024-07/files/15368652/-.pptx) +- [수행결과보고서](https://github.com/kookmin-sw/capstone-2024-07/files/15329735/default.pdf) +- [최종 보고서](https://github.com/kookmin-sw/capstone-2024-07/files/15426987/final.pdf) diff --git a/_config.yml b/_config.yml deleted file mode 100644 index c741881743..0000000000 --- a/_config.yml +++ /dev/null @@ -1 +0,0 @@ -theme: jekyll-theme-slate \ No newline at end of file diff --git a/ai/.gitignore b/ai/.gitignore new file mode 100644 index 0000000000..181d423139 --- /dev/null +++ b/ai/.gitignore @@ -0,0 +1,177 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +__pycache__ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python \ No newline at end of file diff --git a/ai/Dockerfile b/ai/Dockerfile new file mode 100644 index 0000000000..2b5e4713bd --- /dev/null +++ b/ai/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.9-bookworm + +ADD . /app +WORKDIR /app + +RUN python -m venv venv +RUN venv/bin/pip install --upgrade pip +RUN venv/bin/pip install -r requirements.txt + +CMD . venv/bin/activate && uvicorn check_api:app --reload --host 0.0.0.0 --port 8000 \ No newline at end of file diff --git a/ai/check_api.py b/ai/check_api.py new file mode 100644 index 0000000000..128e010716 --- /dev/null +++ b/ai/check_api.py @@ -0,0 +1,34 @@ +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +import pandas as pd +import re +import random +from sklearn.feature_extraction.text import TfidfVectorizer +from sklearn.linear_model import LogisticRegression +from sklearn.model_selection import train_test_split, GridSearchCV +from sklearn.metrics import accuracy_score +from joblib import load + +app = FastAPI() + +class TextData(BaseModel): + message: str + +model_bundle = load('model.joblib') +model = model_bundle['model'] +vectorizer = model_bundle['vectorizer'] + +def normalize_text(text): + return re.sub(r"\s+", "", text) + +@app.post("/predict/") +async def predict_profanity(data: TextData): + normalized_text = normalize_text(data.message) + transformed_text = vectorizer.transform([normalized_text]) + prediction = model.predict(transformed_text) + is_profanity = prediction[0] == 1 + return {"profanity": bool(is_profanity)} + +@app.get("/health-check") +async def health_check(): + return {"status": "ok"} \ No newline at end of file diff --git a/ai/dataset.txt b/ai/dataset.txt new file mode 100644 index 0000000000..de839ee25c --- /dev/null +++ b/ai/dataset.txt @@ -0,0 +1,5833 @@ +좌배 까는건 ㅇㅂ|1 +집에 롱 패딩만 세 개다. 10년 더 입어야지 ㅋㅋ|0 + 개소리야 니가 빨갱이를 옹호하고 드루킹을 ㅇㅇ짓이라고 말못해서 삐진거야 빨갱아|1 +세탁이라고 봐도 된다|0 +애새끼가 초딩도 아니고 ㅋㅋㅋㅋ |1 +731부대의 후예라 그런지 가학적인 아이디어는 세계최고임 이래서 애교만 떨어도 돈 벌리는 한국에 기를 써서 진출하려고 하지조센남자들은 또 이쁜여자만 보면 사족을 못쓰며 공주대접해주는 놈들이니|1 +재앙이한건햇노|1 +글쓴이 와꾸 승리에 비하면 방사능 피폭 원숭이 일듯..|1 +마 씨발련 아 몇평이고 맷개드갔노 니 대하이햄하고 해밨나|1 +은행에 대출 상담 받으러 가보면 직업의 귀천 바로 알려줌|0 +ㅋㅋㅋㅋㅋ|0 +ㄹㅇㅋㅋㅋ|0 +우리지역군데 금태섭 뽑으면 안되지? 그래도 자한당 뻡아야겠지?|0 +꽃다발사들고 알바하는곳찾아가서 무릎꿇고 '내 마음을 받아줄래 지영아?' 하면 바로 넘어온다 여자들 이런거 존나좋아함|1 +박근혜 안빠는데 보수통합 3원칙 인정함 |1 +대가리에 필터없는 연봉 30억 강사vs대가리가 없는 용접공|1 +백정주제에 라고하는걸 비하라고 느낀다면 본인조차 직업에 자부심이 없는건데 |0 +이게 다 백인 탓이랑께|0 +뭐저런 골빈 ㄴ이 다있냐 ㅋㅋㅋ 저런게 학원강사라고? 애들이 불쌍|1 +마감시간 다돼서 떨이하는 고기 사먹으면 식중독 518퍼네아 오래 된 것도 새 거처럼 둔갑해서 다음날 팔테니 떨이라는 개념이 아예 없을라나 |0 +열사발 들이켜도 아깝지 않음|0 +정신병자 천국이구나|1 +으따 옷 이딴건 필요 없당깨 현금으로 달랑깨 !|0 +?|0 +난 사이트 마다 아이디/비번 다 다르고 난이도 높음엑셀 파일에 모든 아이디/비번 / 계좌정보 등등 다 들어 있고이 엑셀 파일에 비번만 기억함|0 +깍새들이 시발 존나강요하더라|1 +리뷰쓴 새끼가 쓰레기 맞지저건 ㅋㅋㅋ 존나 악랄하게 사진찍어 올리는데|1 +ㅋㅋ ㅇㅈ 저런거에 앉아어 감전사 당할 수도|0 +틀딱 씨발새끼들아 다꺼져라 냄새나니까|1 +근데 그것 요즘 살짝 트렌드 지났더라.|0 +군대에서 여군이 필요한가|0 +ㅋㅋ복수할겨|0 + 내가 세금 안내면 너 같은 동사무소에서 프린트나 하는 새끼는 월급 못받고 굶어 뒈져야 돼 병신새끼야.|1 +면도 뭔 스머프모자 쓰고왓네|0 +사형 예지!|0 +당연히 많이볼수밖에...|0 +흑종원 ㅋㅋㅋㅋㅋㅋㅋㅋ|0 +=> 난 그런 말 한 적 없는데? ㅋㅋㅋㅎㅎㅋㅋㅋ왜 거짓말하노? ㅋㅋㅋㅋㅋㅋㅋㅋ|0 +원래 수준이 그래|1 +호주에서는 남자들이 동물보다 못한존재라던데ㅋㅋ|0 +나베가 나베했네요!|0 +전에 제주도 여행 갔는데 폭포 앞에 사진 잘나오는 포토존에서 사진 찍는거 기다렸는데 앞의 6명의 짱게이 때문에 짜증남먼저 6명 각자 한장씩 찍음다음 번갈아 가면서 둘이나 셋이서 짝 지어서 찍음 정말 끝나서 싶어 찍을려고 하니 그 6명이 다 모여서 포즈 바꿔가면서 또 사진 찍음이지랄 하고 있음|1 + 행함이 없어도 불경건한 자를 의롭다 하시는 그분을 믿는 사람에게는 그의 믿음이 의로 여겨지느니라.|0 +짱깨들 말하는 거임? 짱깨국이 무상의료라고?|1 +빡대가리들에게 방법을 가르쳐주시면 ㅠㅜ|1 +누가 뭐래냐?|0 +시발 정작 지네들 자식은 몸쓰는일 안시킬거면서 개지랄떠네 ㅋㅋㅋ|1 +용접협회 무시하지 마라. 저기 현장 고수및 용접에 관한 전문지식 가진 대학 교수 학자들 모임이다. 파워력 쎄다.|0 +너도 해|0 +안하는 애들도 결국 학업에 흥미를 못 느꼈다는거니까|0 + 니들 틀딱들 닭빨고 진빨고 중국빨고 그때그때 빠는새끼들임 닭이 친중하면 중국도 빨고 오성홍기절하고 아님? 억울하면 논리적으로 반박을 해 ㅋ|1 +그냥 홀딩하세요|0 +만 나이 공식화도|0 +꼴리지는 않고 그냥 존나 귀엽다 |1 +그냥 선거철에 지 지지자 결집시키는거임 자기 지지층엔 아시아인이 별로 없고 이민자들 싫어하는 사람들이 대부분이니까|0 +오마쥬가 뭔진 앎?|0 +4카 성공|0 +쓰레기들은 휴지통으로|0 +크로캅이 본야스키 개털던거 생각나네 ㅋㅋㅋㅋㅋㅋ진짜 개잡듯 잡아팼지 ㅋㅋㅋㅋ전직 은행원 본야스키 ㅋ|1 +용팔이|0 +그러니깐 딜도로 쑤셔박아야한다는거지?|1 +그래서 씨발롬아 PC가 나쁘다는 거야 좋다는 거야?|1 +미쳤나 진짜 시발|1 +나 키 183인데 사람들끼리있을때 누가 자기키 177이라하면 나는 "어? 내키는 175인데?"해주면 주변사람들이 오오 너희 둘이 키재봐라 하는데 키재면 내가 더큼 ㅋㅋㅋ 슬퍼하는모습보는거 개꿀잼|0 +아..이분 볼때마다 심장이 뭉클해지네|0 +ㄹㅇ 시위 진압 ㅆㅅㅌㅊ였음|0 +정재야 니 와그라노|0 +뭔가 잘못이해한거같은데 문희상 천황드립이란 소린 문희상이 천황으로 말해서 잘못했다는게 아니라(애초에 일왕이라고 함) 지금 천황이 전범 아들이니까 위안부에게 직접 사과시켜야한다는 말이 불러올 파장을 생각도 못하고 경솔하게 대처해서 빌미를 줬다는 말이야|0 +진짜 몇시간씩 할 정도면 히로뽕맞고 하는거일수도있다경교대있을때 뽕쟁이 재소자 의무실간다고 계호할때인데 내보고 히로뽕하는게 단순하게 기분 좋은것보다 목적이 ㅅㅅ라던데비아그라보다 효과 좋단다 ㅋㅋㅋ|1 +휴게소, 마트 등의 여성전용주차장 보고 분노를 해야지|0 +질렀으면 끝을 봐야지 ㅆㅂ 만나자해서 이야기해라 머리채 잡고 내가 너 좋와하면 안되냐 그담 허리띠를 풀고....|1 +앞머리 좀 자르던가 ㅅㅂ |1 +아 그런거야? ㅋㅋㅋ|0 +영혼까지팔아도 된다는놈일거야|0 +그때 조언했으면 멱살부터 잡을놈이ㅋㅋ|1 +남친이노?|0 +진주네요...여기 동네에 잠시 살았는데...여기 주차 장난아닙니다.....그냥 욕만 나오죠...|0 +대구경북 당선조건 : 매국노|1 +개찐따 답네 ㅋㅋㅋㅋㅋㅋ|1 +노가다 다니기 싫으면 노력해서 살지 그랬냐 틀딱들아 ㅋㅋ니들 어릴적 꿈이 용접사였냐?상황이 ㅎㅌㅊ가 되니까 그나마 할만한 용접이나 한거고 하다보니 돈도 되고 존나 힘든데 보람도 느끼니까 사람된것처럼 느끼나 본데노가다는 노가다임 주제를 알고 살자|1 +피파 회장도 보냈잖아.|0 +우리나라였으면 남성1년 집해유예, 엄마 20년 때릴것같네요. 아님말구...|0 +와 석유 나는 나라의 위엄이 바로 저런거로구나. 근데 저런 선진국이 될수 있는 나라가 일부 새끼들이 그 부를 독점하려고 차도르 씌우고 전쟁광 놀음 하고 있는거잖여 |1 +가봤는데 11만원정도면 차라리 걍 호텔뷔페감호텔뷔페에 랍스터가없는것도 아니고 그냥그랫음|0 +나 안동김씨|0 +새로오면 다 그렇게함 열심히 하는티를 팍팍내고 뭐 바꾸고 뭐하기로 하자 압박을 주잖냐 10년간 너무 타성에 젖어살았던거 아니냐? 위기감없이|0 +ㅂㄷㅂㄷ ㅋㅋㅋㅋㅋ 중국 욕하니 ㅂㄷㅂㄷ 하죠 ㅋㅋㅋㅋㅋㅋㅋㅋ|1 +정치인위에 문재앙 |1 +오이 오이 아부네|0 +무게치는거에 인생버린새끼들|1 +빌런vs빌런|0 +저거 안비싸|0 +가세연에서 정규재 위장보수 분탕이라고 좆나깜 ㅋㅋㅋㅋㅋ |1 + 수술도 남자 의사가 잘한다|0 +이봐요 미친놈씨|1 +그러나 그건 수치상일뿐 현실은 굉장히 망할 가능성이 높은 나라지|0 +여자들 물뽕 처먹여 강간하고 동영상 찍은게 죄가 아니었구나 ㅋㅋ버닝썬이 중국 삼합회 자금 세탁소라면서?|1 +이게 여자다|0 +키 170 27살 따먹은여자 23명 ㅁㅌㅊ?|1 +막상 마주치면 쫄보 새끼 마냥 쳐다도 못 볼 새끼가 조디만 살아가 ㅋㅋㅋ|1 +저런 도로에서 정차 하는거 봤냐? 당연히 저상황이면 4p가 아니라 브렘보 조상님이 와도 못세움. 내말은 최소 50키로 이상은 달릴꺼라고 가정하고 같이 도로를 공유하는건데. 시발럼아 그리고 저 사고는 차간거리가 문제가 아니야. 정차가 불가능한 도로에서 선건데 씨발. 참고로 블박차도 무적권 과실은 잡힘. 내가 말하는 적당함이라는건 사회적으로 용납이 가능한 암묵적인 룰 같은거지|1 +백종원 음식 너무 자극적임|0 +거짓말아니고 소방관들 저런거 존나 흔하다내가 입 털어도 몇 명 구속되겠네개방관들 진짜 개새끼들임|1 +우스갯소리로 많이하던거니까|0 +박정희만세 문재인 개새끼 김대중씨발새끼 전라도 폭동|1 +빠따로 전신마사지해주는데 모든사람들이 너무 기분좋았는지 엎드려서 잠만자더라|1 +1절만해라 짤게에 또 글올리지말고 ㅋㅋㅋ|1 +이만희총회장님께서 신천지구원으로 전세계에서 몰려 온다 하셨습니다.|0 +음.. 유튜브안하니?|0 +누가 따라다니면서, 교통법규위반 계속 신고할듯.|0 +와 근데 너가 만약 짤같은 상황에서 성공하면 존나 꼴릴것같다.|1 +극혐이노 ㅅㅂ ㅋㅋㅋㅋ|1 +난 야동도 교복입은건 안본다 에라이|0 +띤도, 최서현 house party 추천 개띵곡|0 +개우끼노ㅋㅋㅋㅋㅋㅋㅋㅋ|0 +공지이잉 ㅅㅂㅋㅋㅋ|1 +내말이 ㅋㅋ 아직 세상물정을 잘 모르는것 같더라...사회생활이 적었나..|0 +다른 건 그런데 문씨거는 감동이다|0 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋ ㄹㅇ|0 +강력계 뽑을 때 유도국대출신들 특채 있다 무도특채. 그리고 강력계 들어가면 스스로 권투도장 다님. 아참 레슬링선수출신 특채도 있었음|0 +시스템은? 다 지 좆꼴리는대로 무법천지지|1 +적당히를 넘어 꽐라될때까지 마셔서 문제지|0 +새벽 2시 반에 고소장 제출이 가능한 나라가 세상에 어디있냐|0 +창의력 인정|0 +일베새끼들은 그냥 무식한거 맞네 ㅋㅋ|1 + 내가 시키는거 가능? 닭 병신 종북짓한 병신 ㅇㅇ 해봐 ㅋㅋㅋ|1 +꺼져라 씨발 검머외 새끼들아|1 +저 사건 판결문 찾아서 봐라 4,000만원 정도 지급 명령 받았음법원이 그렇게 판결 냈다는데 왜 니가 아니라함?니가 노동부 장관이라도 됨?|0 +나도 존나맛없어서 다 버리고 햄버거 시킨거 리뷰 올린적 있는데? 내꺼 찾아서 보내도 레전드겠노ㅋㅋㅋㅋ씨발 맛없는 음식점은 다 장사접고 나가 죽어야함|1 +난 프라이드 즐겨봤는데ㅋㅋ 사쿠라바, 고미 다카노리, 반달레이 실바, 노게이라 등등 개추억이네ㅋㅋㅋ 연말에 남제? 이런거로 빅매치뜨고|0 +눈주변 시컴한 인간들치고 정상인 없더라 |1 +대가들...|0 +대가리를 밟혀봐야 정신드는 유형|1 +왜 연돈만 저지랄이냐? 예전에 그 튀김덮밥집도 존나 칭찬했는데 거기도 줄서냐|1 +굳이 한자까지 섞어서 글올리는이유가 머냐ㅋㅋㅋ|0 +그 쪽도 매일 그러잖아요. 공적 마스크 시행한다니까 바로 사회주의 타령. |0 +심해공포증은없는데 우주공포증있음|0 +아~~ 짭ㅈㅎ주고싶다 |0 +서울은 조선족이 몰리는곳|0 +ㄹㅇ ㅇㅂ|0 +화산지형이라 나쁘진 않을껄? 강수량 많아서 물 좋음...|0 +전라남도 안정권 홍어새끼 좆이나 빠는새끼가 가세연을 까노 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +선동질 당하는 니네가 불쌍타 ㅠ|1 + 제발 못배운티좀 내지말아라|1 +2016년 12월 기사를 가져와서 올려놨으면 그 이후 어떻게 되었는지 설명이라도 좀..|0 +아니 시발 이걸 정보라고|1 +정신 못차렸네|0 + 자원으로 연명하는 나라라서 교육수준이 ㄱㅆㅎㅌㅊ임|1 +굴다리밑 아들 국적이 ㅋㅋㅋㅋ|0 +할마시들이 가져가겠지|0 +ㄴㄴ 외모가 전부는 아니라고 생각함|0 +필리핀은 아예 경찰이 돈을 갈취함 ㅋㅋㅋㅋ|0 + 개븅신 저능아는 너고 이 병신 새끼야 ㅋㅋㅋㅋㅋㅋㅋㅋ|1 +뭐 10조쯤은 껌으로 땅사는 기업도 있는데....|0 +고참이 딸려들어간 스티로폼 씹을 때마다 희열을 느끼는 거야 ?|0 +딴따라들 노가다 딱한번 뛰고오면 자살같은건 배부른걸 느낄건데|1 +연초밥|0 +대하이햄만 찾으면 협상 OK|0 +어차피 여자는 성공한 남자의 전리품에 불과하다 |1 +누군들 중동여행 가보고싶지 않겠냐|0 +직모는 답없다 그냥 무조건 스포츠짧은머리 모히칸 들어가야된다|0 +뚜렛사슴 ㅁㅊㄴㅋㅋㅋㅋ 잘봤다 나도 주위 눈치가보여서그렇지 일본여행가고싶어 죽겠다 추운날 온천담그고싶노|1 +번호판 세자리냐?|0 +나 거기에다 합성아니라고 했어|0 +개새끼가 처음에 잠복한거 다 알아보고 할수없이 놀아주는거네 ㅋㅋ속으로 하...저병신년 또 저지랄이네 했을듯 |1 +젖탱이는 인정 , 자연산에 크기도 존나 크고 모양도 , 문제는 면상|1 +버러지들|1 +이게 말이 되냐?|0 +금발거유=무뇌 라는 스트레오타입|0 +일단은 표현의 자유로 남겨놔야하는 것 같음|0 +그럼 20분짜리영상 매일올리면 한달에 600임?|0 +그예로 벌써 중국이 마스크를 보내주었구요.|0 +우주의 기운이나 받아라 멍충아|1 +저건 용접공 비하라기 보단 저성적자들을 비하한거 아닌가?|0 +레알 200만원이 2000만원 가치임?|0 +한번씩 바람쐬러 일본가는 데 일본 가보니깐 동네마다 복싱 체육관이 있더라 오사카 나니와구 가니깐 존나 후미진 공단에도 복싱체육관있음 진짜 울나라 80년대 쓰던 파랑색 날개 처 돌아가는 선풍기 하나 달린 개거지 같은 체육관에서 복싱하고 있는 거 봤다 개쪽바리 새끼들보면 복싱 엄청 좋아하는 듯복싱만화도 많고 이렇게 복싱 씹덕들이 많으니 발달 할 수 밖에 없는 것 같은데 그래봤자 뭐하냐핵폭탄 처 맞고 살가죽 녹아내린 새끼들인데|1 +ㅋㅋㅋㅋㅋㅋㅋ|0 +일베줬다. 티비 밑에 받침대 정보좀 줘. 어디꺼임?|0 +곧 샤오미에서 85인치 나와서 삼성 바를듯|0 +응 결혼해서 애낳는순간 칼각이고 지랄이고 그런 습관 다 없어짐. 돌아서면 아기가 씨발 다 해쳐놓는데 무슨 정리는 얼어죽을 정리|1 +그게 팁이있냐?|0 +강사도 누구나 함 주둥이만있으면|1 +돈 좀 빨아먹으려고|0 + 진이 과거에도 대놓고 욕했으면 질투로 안보고 열등감으로 안봄 아직도 이해안됨?ㅋㅋㅋㅋㅋㅋ|1 +30|0 +운지햐라|1 +동남아 가면 식중독 많이 걸림 ㅇㅇ 현지인들도 자주 걸리는데 우리보다는 강하더라 ㅋㅋ 길에 파는 음식 재료도 재료지만 사용한 그릇이나 컵을 흐르는 물에 씻지않고 그냥 양동이에 첨벙 담궜다가 걸레로 닦음 ㅋㅋㅋㅋㅋㅋ그리고 개네들 길에서도 쿰쿰한 냄새나는게 그 양동이 물이 못쓸 정도로 더러우면 그냥 길위에다가 부어버림 ㅋㅋㅋㅋㅋㅋㅋ 아 물론 조선에 길거리 음식도 다를바 없다고 생각한다 ㅋ|0 +아 이 개병신새끼씨발ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +어허 ~ 이상한 말 하면 안댕|0 +지금 맹구는 돈이 얼마들든 사람 구실만하면 다 혜자라 그러는듯|0 +아하|0 +저래도 찍지 그렇지|0 +이름에 존 들어간 애들이 쌔다 제임스는 안됨|0 +남양주병. 힘내자|0 +나만아니면돼~|0 +전라도 홍어년놈들이 주는 거 사주는 거 사양하고 안 먹는 게 가장 안전하다|1 +혼전동거와 피임이 진짜 중요한거같은데 우리나라는 동거도 존나 안좋게보고 툭하면 속도위반에.. 성인끼리 만나서 도장찍고 헤어지는거야 진짜 아무것도 아니고 그냥 실수인데 애들은 뭔죄냐 사람이 평생 뒤질때까지 실수하면서 살지만 그 실수로 돌이킬수없는 상처가 남는 실수는 조심해야된다애낳고 하는 이혼도 그런실수고|0 +왔어? 안자고 왜 왔어?|0 +거기서는 남자들이 디엠보내고 따먹음 ㅇㅇ 너 얼굴이 1차적 공략법이지 다른 방도는 없음 걸레들이 외모 더 따짐|1 +저런애들이 아다랑 스시녀 찾았던 거냐?|1 + 하며 뒷짐지고 싸우는척 하는 웰빙 샛끼들 천지 삐까리란다.|1 +딸 너무 걱정하지마~ 사춘기 지나고 철들면 알아서 잘 산다 |0 +왜??|0 +공부못하면 기술배워라는 말이 있잖아.|0 +부러우라고 올린거 아니니까|0 +화곡동은 뭐냐|0 +내가성경대로믿기시작한건 창조과학회김명현박사 영상보고 ...무적권 성경대로 믿기로 한거거등|0 +일부러 팔꿈치 좌석밖으로 걸쳐놈 ㅋㅋㅋ|0 +저 상황에서 남 목숨 챙기는 병신새끼도있냐 ㅋㅋㅋㅋㅋㅋㅋ|1 +경찰 vs 군무원 어떤게 더 낫냐|0 +ㄹㅇ 똑같은 업무지시를 해도 사람 진짜 존나 빡치게 만드는 재주가 있음|1 +어째 재네들은 제대로된 놈이하나없어|1 +물가상승률 고려해라. 20년 지나서 3억이면 지금 시세로 5천만원정도 될꺼다. 너한테 지금 5천만원 물려주고 독립시켜주는거랑 같은 상황.|0 +걍 니뽕끼리 전국체전이나 해라 ㅋㅋ|1 +틀딱이네 --- 먹을 나이 됐다|1 +느개비 인증해라 엠생충새끼야 ㅋ빤스충 엠창새끼 ㅋ|1 +일본인들은 한국인이 아직까지 똥술 먹는다고 믿잖아|0 +장관 임명권은 대통령한테 있는데 저딴 종이조가리가 뭔 ㅋㅋㅋ 틀니 다 뽑아버릴라|1 +일본은 칼을 사면 칼날이 잘 드는지 길가는 사람들을 죽여서 확인했던 풍습이있다.|0 +당연히 2017년의 표적이 아닐수도 있겠지 |0 +저기 어디임?? 저기가 ㄹㅇ 무릉도원 아니겠냐 ㅋㅋ뭐 보면 모델들 데려다 컨셉사진 찍은거 같기도하고 괜히 서양이 공주님 같은 이야기가 있는게 아니네|0 +류석춘 교수는 진짜 닮았네 ㅋ|0 +ㅇㅇ벽돌날라 김씨|0 +ㅇㄷ|0 +법이 ㅡㅡ 와일노|0 +모르고 당하면 갑작스러운 사태에 혼돈 그 자체일 사람이 얼마나 고통스러울지 생각해봐|0 +이란이 통제 당하고 있어서 시위하나 안날거라던 병신새끼들 어디갔노|1 +철대패로 저놈들 흑두 네알 싺싹 밀어서모래밭에 파묻고 싶다가능하다면 동작동가서 두개골도 빠사버리고 싶음|1 +근데 찍어도 7등급 나오기 힘들다던데어케 해야 7등급 나오냐 ㅋㅋ씨발 그것도 대단하다|1 +그래도이새끼..어느정도 친구에뜻을이해햇는지.. 친구들한테 그간미안햇다고 단톡날리며 집들이할테니 와달라고함..|1 +남편이 스시남이더만|1 +저기도 댓바기지 생략한거임|0 +평소일베같지않노? ㅋㅋㅋ 주예지가 물론 잘못한건 맞지만일베도 노가다 혐오 극에 달하는데 ㅋㅋ|0 +얼굴이 다른 사람인데?ㅋㅋ|0 +무조건 학부를|0 +잘 해봐라|0 +러시아 맞음 서구권 백인 소아성애자커뮤니티한테 러시아는 |0 +7등급은 가르쳐서 되는 게 아님. 그냥 멍청한 학생이 안배우는 거임.그리고 그 전에 문과 1등급 vs 이과 7등급 이랬던거 보면 그냥 이과부심 부리면서 주제모르고 나대는 병신일 듯.|1 +ㅅㅂ 인구가 준다는데 수도권에 더 많이 살아. 좀 갈사람은 나가라. 특히 586세대들|1 +난 내가 코작다고 생각하는데 가족이나 친구들 다 코 크다고 그러더라|0 +아니 메시 허락은 안받고 아버지 허락을 처받고있냐 미친 양아치 새기네|1 +미친 짱깨년이 양키스 비니쓰고다니네 ㅋㅋㅋ|1 +나형 백분위 1은 좀심한거아닌가수시합격하고 친거같음|0 +레아 미만잡 ㅋㅋ총밖에 못쓰는년이 공주지위 하나로 오더존나내림|1 +두부는 외상으로|0 +힘들겠지만 깔끔하게 연락 끊어라여자가 관심있으면 한달안에 연락온다한달 넘어갔을때 연락 없으면 끝 그때도 니가 지금처럼 죽고싶으면 그때 죽어라|0 +그레서 어떡계하라고 ㅡ , ㅡ|0 +아`~~~저기 가입된 사람 전자발찌 가자............이런건 전자발찌 가도 국민들이 박수치지 뭐라 안한다...|0 +저거 유튜버들 다 찍었어 선동 ㅁㅈㅎ|0 +해체가 답이지|0 +이제 박사모들이 곧 저 여자 김무성계 김영삼계라며 합리화시킬거임|0 +저거 남자쪽에서도 저렇게 적게 받는건, 매달 나가는 돈 아니까 저렇게 적게 받는 경우가 많음.내 생각에 결혼하기전에 5000만원 정도는 비상금으로 가지고 있어야 함.그리고 결혼하고 생활비 다주더라도 가지고 있떤 5000만원 주식이든 펀드든 뭐든 넣어 불리면서 자산을 만들수 있어야 어디가도 기가 안죽음|0 +카엔... 주인공보다 더 꼴림|0 +거미누나 부럽습니다|0 +그래서? 잘생겼으니 인기끈거고 정치병 유투버도 아니고 지 능력으로 뜬건데 질투라도 해주길 바라냐?ㅋㅋㅋ|1 +너의 주장에는 확신만 있고 근거가 없다.|0 +대구 경북이 문제|0 +능지 병신들임 ㄹㅇ|1 +오다기리|0 +그정도니까 추첨하는구나..|0 +김일성이 중국말 러시아말을 하는 조선족짱깨라메?? 그거 보고 충격먹음 시발 지금 우리가 중국이랑 싸우고 있다는 거자나|1 +스티브유 그냥 지들 나라에서 살기를..|0 +결제한세끼들도 다 잡아족쳐야지|1 +몽골한테 "최근에" 250년간 지배당하고 집단강간을 당했는데 동양피가 하나도 안섞였다고 쌩어거지 떠는 초졸또라이보소 엌ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ. 니가 말하고도 너무 어거지라 좀 부끄럽지?? 엌ㅋㅋㅋㅋㅋㅋ|1 +우리개는 안물어요 ㅎㅎ|0 +씨벌련이 닉변하고 아침에 올린거 토씨하나 안바꾸고 그대-로 좆중복 조져버리네 씨벌련아 ㅁㅈㅎ|1 +이참에 그년 보지나 용접해 버리자|1 +곧 남자가 혼자 다 하겠네 ㅉㅉㅉ|1 +요즘은 야스오 픽하는 애들 50%의 확률로 잘하더라..|0 +재앙이가 낙연이로는 불안한갑다|0 +ㅋㅋㅋㅋ.오늘 처음으로 소리내서 웃었다.|0 +참고로 뉘른베르크 유망주 시절에 얘 키워준 감독은 덕배 월클 만들고 14/15 볼북으로 리그 2등, 포칼 우승 시킨 디터 헤킹 ㅋㅋㅋ디터 헤킹 - 위르겐 클롭 (뢰브) - 투헬 - 펩진짜 귄도안 감독복 오짐|0 +술먹고 판사새끼 죽여도 무죄|1 +자나깨나 정치생각 ㅋㅋㅋㅋㅋㅋㅋㅋ|0 +와 최대1만명 수준이면 최소 몇천은 될것같은데....다 잡아서 콩밥은 무슨 자지다썰어야됨.|1 +장담하는데 방송이라 종아리라 그런거지 현실에선 젖꼭지임|0 +홍어들은 씨를 말려야함 |1 +NTR 하면 된다 이기 바꿔먹으면 된다 이기|0 +오 미키 너와나의 미키 헼 헼 오 미킼!|0 +니 애가 불쌍타너욕하는게아니고 진심임애는 뭔죄냐|1 +진짜 정신이 나갔네|0 +어찌보면 반달이 젤 애매하고 경우따라 더 조심해야될 부류일지도|0 +와 이젠 일베할떄 위도 살펴봐야겟네 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ경찰이 수습한다고 폰 주웟는데 일베틀어져있으면 ㅈ되겟다 |1 +저 등치에 은근 발밑좋고 탈압박조음ㅋㅋ 생각해보면 야투랑도 비볐던놈이니까|0 +치매|0 +난 저러면 좆진지하게 말그대로 답변을 해줌|1 +저런집에 자식들 용돈 얼마주냐 그러면대부분이 50씩 쓴다 그럴거다 ㅋ가장이 미성년자 자식한테 용돈 빌려쓰는 괴상핫 집구석|0 +성당사진 퍼온 건줄 알았는데 현판같은 거노?|0 +음식전부 원단가 솔직히 1만5000원 절대 안넘는다 목아지 건다|0 + 그게 너가 세상살아가는데 제일 합리적인것 같다|0 +작가님|0 +그냥 유행이라도 따라가라 자기가 진짜 개성있게 옷잘입는거아니면 유행만 따라가도 반은간다 ㅇㅇ 옷입는거한정|0 +내 댓글이 마운트 까는게 주 포인트는 아니긴 했는데나도 마운트 좋아함 하드워커에 에너지풀해서 ㅎㅎㅎㅎ|0 +박사야!|0 +씨이발|1 +세상만사는 니가 생각하는것보다도 훨씬 더 복잡하다.|0 +일베는 뭐든지 다 깜 그냥 아가리로 까기만함 ㅋㅋㅋㅋ거기까지 만함 ㅋㅋ 행동 일절 노노 ㅋㅋㅋ|1 +글쎄 옆집 아저씨가 사줬나부지뭐|0 +나 K2 흑열전차 실제로봤는데 위압감 미쳤음 ㄷㄷ;;K9 자주포 K2 흑열전차 ㅆㅅㅌㅊ 밀덕되는이유를 알겠더라|0 +죽여버려야지 저런놈은|1 +진짜 본인애니보고 딸치게 생긴 씹덕 얼굴상 얼굴깐글에 욕도 박을수있으니 김치응딩이 ㅅㅌㅊ|1 +아가리가 왜저렇냐|1 +이건 김성령이지 |0 +난 재미교포 크리스리 그년은 진짜 느끼면서 하는년임|1 +복싱에선 여태 일본이 많이 쳐발렸다..|1 +어차피 기존의 항암치료 다 하면서 보조적으로 먹는 것치기 때문에 밥줄 끊길 일 없음. 펜벤다졸만 먹고 암이 호전됐다거나 치료됐다고 하는 사람은 없었다|0 +벤치에서 꿀잠자고있던 아재, 어떤 20대놈이 그 아재 아리랑치기하고 사까시했던 짤방 진짜 쇼킹이었음|0 +ㅋ free radical 과 antioxidant의 균형이 깨져 oxidative stress 가 이뤄지는건데, 니 말마따라 산화과정 자체를 스트레스라고 치면,|0 +외과의사 만큼은 최고로 쳐야한다.|0 +세스코도 졵나 감당안되는곳은 그냥 물건 싹다버리라거나 포기함|1 +엑소 중국놈들이 통수쳐서 그후로 sm에서 중국국적 제대로 안키우더만. 아무튼 아이돌 산업 그거 생 구라 아니냐. 저 새끼들 중에 안쳐대고 다니는 놈 없을걸. 저런데 돈 쓰는게 병신호구지. 유사 연애감정으로 돈 버는 주제에 한번에 임신에 결혼까지 해버리네.당연 팬들 입장에선 빡치지. 아무튼 이런걸 알아서인지 제대로 된 남자는 딱히 좋아하는 연예인 없음.|1 +순 연봉만 따지면 비비는데 광고 이런데서 딴 종목이 압살해서 그런듯|0 +기술-공대-엔지니어기능-공고-테크니션기능공들은 기술인이라고 구라차지마~|0 +이거 ㅇㅂ 주는새끼들좆뉴비새끼들이냐? 본거 또보고 또보고 꾸준히 쳐 올리네.좆망 병신 사이트 ㅉ|1 +위조된 신분으로 학교를 들어가다니|0 + 생물학적으로 번식가능한데 사회화를 위한다는 이유로 억압하고 있는거지|0 +3대몇|0 +니 꼬치다ㅋㅋㅋ|1 +제발좀 잡아가두자~~|0 +저 양반은 어쩌다 저리 됐을까요?|0 +꼬시다 꼬수워노가다 세끼들 제발 좌파해라 쪽팔린다|1 +조국 사태보면 박탈감 느낄만하지 근데 백수새기들은 그냥 노력안해서 취업못하는거|0 +언론사어디고?|0 +역성장이 아니구|0 +얘도 헌법 강의해서 조무사 되면 저 사태 해결 가능함|0 +땅콩 사무장양반 중간에 낑기게 한게 더얄미움|1 +ㅋㅋㅋㅋㅋㅋ어이가 없노|0 +와.... ㄷㄷㄷ비호세력이 있는지 탈탈 털어야됨|0 +어휴 조선 새끼들은 그저 꼬투리 잡는건 기집이나 사내 새끼들이나 똑같다. 왠만하면 하는게 용접이니 비유를 한거 가지고 아직도 이지랄들을 하고 자빠졌네|1 +개소리ㅁㅈㅎ|1 +목도리 같은 건 펜디|0 +과거 글이 없잖아?|0 +좀 그만해라 씨발새끼들아ㅋ|1 +기승전 베들베들 목적에 충실한 벌레구더기|1 +ㄷㄷ|0 +이러고 그래 나도 좋아|0 +나잇값 못하는 발기부전 땅꼬마|1 +저러다 뒤지면 후방주시 태만으로 버스기사한테 책임 묻겠지|0 +한 말을 그대로 적지 않고 변경했지?|0 +씨발년아 웃겨?|1 +이글을 읽어보시면 Navigant Research가 왜 말이 안되는지 (nonsense) 그리고 왜 테슬라가 여기에서 꼴찌로 나와있는지 비판합니다. 일단 Navigant Research는 리포트를 만들어서 $3000~6000 불 가량씩 주고 파는 곳입니다.|0 +픽업아티스트다|0 +가족끼리 싸우지마요~~~~|0 +주민들한테 삽으로 머리를 내리 찍혀야 정신을 차릴려나|1 +싸캐스틱이다 /ɑː /|0 +건강한 생활습관을 가지고 살면 심하게 살찔일도 없다|0 + 니가 정중하게 나오는 걸 보니 본인 하는 말에 큰 뜻을 담았던 것은 아닌 걸로 보이니 더 강하게 말 안한다. 지금 문제가 되는건 중증 '외상'환자이고, 내 의견은 이 처참하게 다친 반송장과 그 처치에 대해 한국의 의료시스템이 변태적으로 되어 있다는 거다. 그런데 저 본문 글 쓴 인간처럼 일부러 사태의 본질을 시스템이라는 식으로 확정 시키고서는 의사 개개인의 열성도 혹은 현행에서의 희생의 감수 따위를 논의 바깥으로 아예 차단시키려는 여러 이기적인 논리적 정황 속에서 읽히는 네 댓글은 단순한 사실진술이 아닌거란 거다. |0 +이석기 옹호했다면서 ㅋㅋㅋ 그걸 설명을 해야지 단편만가져와서 우기는건 노인정용임|0 +어렸을 때부터 키웠던 리트리버 키우고 일주일만 슬펐다 죽음에 너무 슬퍼하면 약해진다|0 +난 이미 2010년 6월 지방선거때 한짓보고 손절|0 +몰랐던 건데 좋은 정보네여..ㅎㅎㅎ|0 +그 연장선에서 박사모틀딱을 다 죽여야 그게 보수가 사는 가장 중요한거임 ㅋㅋ 설명하자면 길어지는데 암튼 틀틀거리는거는 잘못 아님|1 +맞음. 돈이 다인듯 다가 아님.|0 +사살여부가 중요한게 아님|0 +다들 ㅁㅈㅎ 난 ㅇㅂ|0 +저기 올라오는거 99프로가 소설인데 그걸 또 사실인냥 믿고 퍼오는 새끼는 뇌가 있는새끼냐 이런새끼가 민좆당 뽑는 새끼들임 ㅁㅈㅎ|1 +지금 그이야기 하는건데|0 +좆병신 새끼면 담부터 너한테 지랄 못한다|1 +베댓 댓글은 틀딱일베라며 욕함|1 +책읽어주는것도아니고 지식니열해서 별로든데 . 애도 홍어아니냐|1 +도끼로 마빡을 걍.,...|1 +미국도 중국 파산하는걸 원하지는 않을걸|0 +<포춘(Fortune)> 하면 가장 먼저 떠오르는 것은 매년 발표되는 ‘미국 상위 500대 기업’, ‘세계 상위 500대 기업’ 등의 기업 순위일 것이다 --------------> 벌레들 포춘 모를까봐 직접 복 붙 한다.. 정말 우리 나라는 최고다라는 말도 부족하다|1 +입만 벌리면 구라.|1 +8백만페소면 한화로 얼마인지 밝혀야지.|0 +이로서 신라젠은 백퍼네 유사시민새끼가 설쳐되는 이유도 나왔고|1 +시발 이제 만물 김무성이냐, 좌파 새끼들 만물 이명박과 똑같노|1 +조선 수준나옴 힘들게 일하면 대우받는게 당연한건데|1 +갓대중센세 모욕 ㅁㅈㅎ|0 +그럼 전기 발전할땐 이산화탄소 안나오냐?연구마다 다르기는 하지만, 비슷하거나 화력발전이 좀 더 적게 나오긴 한다던데결국 원자력으로 가야 큰 차이 남|0 +치고 나갔겠냐? 우리나라 수준은 솔직히 상용화에 맞춰 물량 생산인프라 구축 수준이지 그 이상은 힘들지만 전기차 기술은 아티펙터 교환, 수명, 급속충전 곧 이 아니라 벌써 시작됬다고 생각한다.|0 +추천~~~~~~~|0 +반대로 해석하면 될듯 현기가 꼴찌|0 +쟤는 친중인척 |0 +국내뿐 아니라 해외도 똑같음약관에 보면 무조건 100% 회사 맘대로 데이터 읽어보거나 활용할 수 있다고 되어있음서비스 클라우드는 털려도 상관없는 데이터만 사용하고 개인자료는 서버 구축하거나 나스 쓰는게 맞음아 메일도 마찬가지임|0 +아줌마? ㅋ 어캐 됨? ㅋㅋㅋ 아직 이야? ㅋㅋㅋㅋㅋㅋㅋㅋ 또 빤스런 ? ㅋㅋㅋㅋ|1 +가형3등급 나형 2등급정도|0 +1주일 안으로 여자가 답 주긴 할텐데,, 안될 가능성이 높음|0 +이시간까지 어쩌고저쩌고 하시면 얼마버는지 말씀드리고 택시 잔돈안받고 걍 내림|0 +취하네요 ㅎㅎ|0 +저러면 어쩌냐 용접 다시 녹일 수 있냐 ㅋ|0 +잠바? 틀딱 ㅇㅂ|1 +그걸 이제 알았냐트위터로 시간이랑 좌표 받고 꼭 3개씩 베댓 올리잖아 쟤네 ㅋㅋ공감순 제일 높이 있던 정부까는 댓글에는 반대폭탄 던져서 내리고.|0 +두 달 밀리면 바로 명도 준비함|0 +좆문가 맞아여...|1 +암사역 시라소니였나 걔 닮았노|0 +기차|0 +과목만 보면 그렇지 근데 수학은 상대평가잖아? 절대평가로 바뀌면 상관없지 ㅋㅋ|0 +찾아보니 역시 인도 최상위계층 브라만임 개부자|0 +벙.신같은게 프로토가 어째 단폴만되냐?개.벙신아 무슨 사설하냐?|1 +참고로 위에 게시물 내용은 |0 +끝난건아는데 다시보고또보고 이지랄하길래요..|1 +슨상님을 안 붙였잖아|0 +아폰살돈은 없지만 저거 살만한 돈은 있는거지 하얀 콩나물 허세부리기도 좋고 딱이지 않냐?|0 +우리나라도 조만간 저렇게될것같음|0 +마 설명을해라 지혼자 아는척하노|0 +셀프 쓰레기통으로...|0 +근데 왜 아직까지 전광훈씨는 밖에서 설치는 거죠?|0 +니 아들래미가 술쳐먹고 아이들 치고 뺑소니 치는건데 실행 불가능한거 아니냐?|1 +대중이 까는거 무적권 ㅇㅂ|0 +데우다? 덥히다?|0 +캬아~이게 낙수효과 맛이지|0 +거기에 대구출신이면 완벽할껍니다.|0 +면도 생각보다 말주변있고 똑똑하다 ㅋㅋㅋ 못생겻다고 무시할 존재가아님|0 +그나마 조국이 말하면 그거 반대구나라고 생각하는 국민이 훨씬많음. |0 +구속 사유가 안된다고 생각은 안하냐??|0 +외국기자들은 자기한테 관심 좆도 없는거 아니까김치기자들 어딨나 찾아보고있노 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +트럭기사들 존나무서운데 가끔보면 센스개지림|0 +러시아인을 동양계라는 병신년은 진짜 처음 본다 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +다른 분들위해서 전 양보|0 +근데 재밌음 게임은?|0 +적자가 계속 되는데도 유지된다는 건 누가 뒤에서 그걸 메꿔준다는 거야.|0 +지랄 10만원은 해야지|1 +괜찮은 아웃풋임. 얼굴도 참하고.|0 +존경 스롭넹 와우!!!!|0 +우리아빠 회사다녀요 이래야지|0 +지네엄마 소개하고잇노 ㅋㅋㅋ|1 +내용에 돈벌이 내용이 나오길래..선정도가 아니라 그거도그냥 범죄니까 사장새끼 18년형|1 +쟨 진짜 좀 이상함이상해서 더 무서움김대중 노무현은 간첩질하는 와중에도눈치보면서 국익에 도움 되는 일 한두개는 했는데..|0 +일본이 더 위험해서|0 +키스해서 빼줘야자|1 +길냥이가 살아봤자 얼마나살겠노.ㅠ 잘해주진 못해도 괴롭히진말자 게이들아 ㅠ|1 +정상|0 +사회에서 대학 말하면 돈이 자동으로 나오누?|0 +수학이라고 떠들래면 미분 적분정도는 메시드리볼해야하는거아이가|0 + 솔직히 나도 성적 수치심 느꼈음|0 +팩트인데 뭘|0 +일베에서 틀딱거리는 새끼들 = 전라도 대깨문 평균나이 50대이상지들이 노인인 줄 모르고 애들 노는데서 틀틀 거리는 거 존나 웃김ㅋㅋ|1 +창렬이노|1 +틀린말은 아닌데?뭐가 문제임?|0 +63,516|0 +맛있겠다|0 +에휴 ㅋㅋㅋㅋ|0 +그만큼 검찰의 사법권한이 약해지는거고 반대로 경찰은 상당히 재량권이 강화된거임|0 +베들베들|0 +라고 해씀져..|0 +몇 급인데? 공무원 개 땡보라고 오늘도 기사 떴드라. 뭐가 적성에 안맞는데??|0 + 평균 학력 고졸따리, 양아치 새끼들 상대하려니까 개빡쳤나보네|1 +중하위권은 일본애들 많이 꼽아주고 좁밥 좀 많았음|1 +그거 제일 잘하는 양쪽 옆나라로 가세요.|0 +지금의 정부는 다 계획이 있구나~|0 +메이플 오목 돈따먹기응용한거네 ㅋㅋㅋ|0 +라떼는 말이야...공포신문 봤었는데...|0 + 누군지도 모르면서 무조건 아니라고 우기고보는게|0 +연분9등법은|0 +뭐 나쁘지 않네요.. |0 +괴물 같은 소리한다데모 안 하고 공부하면 괴물이냐?ㅋㅋㅋ|1 +그말도 맞지, 상류층의 개보지와 하층민의 처녀가 동급수준으로 귀하니까|1 +언제부터 대검이 최씨 대변인 노릇을 한겨?|0 +그래도 좌제동은 성공회대 신방과로 세탁했으니|1 +간병?ㅋㅋㅋ|0 +고생 끝, 행복 시작~!!|0 +진짜 자한당은 하는게 없는 허수아비 정당 쓰레기 좆같은 새끼들 밥버러지 |1 +딱짱깨럽게생걌네|1 +정확하네 나 유치원때 유행했는데 |0 +여자 형제 둔 남성 보수 성향 강해… 美조사 한경닷컴 - 한국경제여자 형제가 인생을 더 행복하게 만든다 - 동아일보여자 형제 없는 남성을 선호한다?_채널A_웰컴투시월드 16회여자형제와 너무 친한 남친, 이건 정말 싫어 - 싱글즈여자형제 많은 사람의 특징이 뭔가요? - - ::: 알찬살림 요리정보 ...여자형제있는 사람이 눈치빠린거 같지않나요? - - ::: 알찬살림 ...여자 형제 있는 남돌과 없는 남돌의 차이 : 네이트판|0 +쓰레기통 리뷰는 처음봐서ㅠ|0 +프리시즌이랑 시즌초반때는 폼이 안좋이서 재계약도 힘들지않을까했는데 중반부터 다시 살아남ㅋㅋ클래스가 있긴있는듯|0 +찌그러져있어 넌 ㅋㅋ|0 +빅버드 나올때 사실 내가 삭~ 도망가서 숨었음.|0 +걍 부산앞바다에 빠져서 죽어라 왜 사냐?? 어휴|1 +시계 보는 게 ㅈㄴ 웃기네 ㅋㅋㅋㅋㅋㅋㅋ|1 +주여 이름만 외치면 구원을 주겠다는 구절도 있지????|0 +야 사업 다 접고 그냥 실업자 되라그래 뭔 씨발 취직을 시켜줘도 지랄이냐?|1 +시베리아 수용소 같은 곳인데|0 +저열한 민족성 미국 이승만 박정희라는 변수가 있어서 본래 민족 모습을 잠시 벗어난거였지 원래 우리 민족 본 모습은 북괴임 다시 그길로 가는게 보이잖아|1 +무선사업때보단 더 좋을수도 있는거 아니노.|0 +사악한년|1 +힘내세요. 너나 할 것 없이 힘든 시절을 보내고 있네요.|0 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +내 비번은 비스무리하지만 사이트마다 대문자 숫자 특수문자 조합 요구하는곳이 있어서 존나 헷갈림그래서 오랫만에 들어가면 이메일로 비번 다시 설정함. ㅋㅋㅋㅋ|1 +민주당이 압승인데 왜 이런걸로 지랄인지....|1 +베충이들 점심때 팀장들 한테 조인트 까일듯|1 +깨끗한 나라 제품‥‥ 잘가라~~~|0 +SUV는 얌체인거고 애초에 블박새끼가 안전거리 미확보에다가 앞차 갑자기 브레이크등 들어왔는데 넋놓고 있다가 박은거잖아고속도로에서는 앞차 브레이크 들어오면 이게 풀브레이크인지 그냥 병신처럼 밟은건지 구분이 안 가니 긴장타고 있어야 한다그래서 안전거리 100km/h당 100m가 필요한거고 블박새끼가 뒤집어 쓰는거임 이건|1 +녹슨 액티언스포츠|0 +0렙인거 보면 모르냐? 분탕치러온 메갈이잖아..|0 +와 이건뭐 진짜 제정신인가 |1 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ좆나하찮게생겨서 너무기엽다 |1 +ㄷ|0 + 지들이 무식한건 지들 잘못이지 왜 남들한테 그 잘못을 씌우냐고 시발아|1 +어려울때일수록 전범국들끼리 힘을 합치겠다?|0 +화장하고나서 나온 뼛가루 종량제 폐기물봉투에 담아서 버려야 하는거라고.....|0 +더지켜봐~~아직이야~~|0 +인생 ㅈㄴ 불쌍하다 흙수저..ㅠ|1 +난 택시기사 자격증인줄 ㅇㅂ|0 +당연한거 아니냐 저기 노하우 자체가 직원 몇명뽑으면 복제가능한 업종이라 저렇게 안하면 회사 망한다|0 +저런 새끼들때문에외동이나 형제만 있는 놈들이남매들은 저럴꺼라고 개같은 상상을 하지. |1 +저건 어제 사건 터졌을때 기사고|0 +물도 꼭 챙겨마셔라 안마시면 죽더라 진짜임|0 +느금마개보지|1 +저새끼 복근은 왜 저런거임??|1 +근데 7등급이면 용접도 못배움|0 +일관성있게 지랄하면 훈장이라도 달아주냐|1 + 1) 민좆당 110% 지지 & 지원 받았고|1 +기레기들아!|1 +와~씨... 집안에 루미나리에 가능하네...|0 +근데 그건 팩트임ㅋㅋ 팩트에 ㅂㄷ거리는 조센1징들|1 +날갤 망함?|0 +진짜 억대 버시는 분들이 이걸보면 무슨 생각이 들까 ㅋㅋㅋㅋㅋ|0 + 일베좌파임.|1 +주작이건 뭐건 웃기다|0 +속보!!!!!!!!!!!!!!!!!!긴급!!!!!!!!!!!!!!!!!!단독!!!!!!!!!!!!!!!!!!!세계 최악의 열등하고 좆미개한 후진국 쓰레기 인종 죠샌징들은 집단학살이 정답임 ㄹㅇ |1 +지잡도 급이 있다 게이야|1 +나 예전에 들은 이야긴데 30살먹은 주노 헤어 디자이너가 자기 월 천 넘게 번다는데 사실임? 믿기지가 않던데 물론 청담쪽이긴 했지만|0 +진짜 메시는 안와상융기가 더 뚜렷하네|0 +누가 유튜버로써 ㅅㅌㅊ인거 모른데? 대한민국 20손가락안에 드는것도 알아 그리고 뭐가 어쨌든 논란이되는 인물은 맞잖아? 페미들이랑 안좋은 접점있고 사실이든아니든 여자친구폭행사건으로 언론에서 오르락내리락거려서 공중파에도 못나옴스윙스같은 남자연예인은 그렇다쳐도 한예슬이 성향이 어떤진모르겠지만 쟤랑 굳이 엮여서 좋을게 없어보인다니까?|0 +그래서 자지짜르고 한국와서 에이즈퍼뜨리고 돈벌고가는건가? 태국년들 뭔가 김치녀와 성격이 너무달라서알고보니 남자새끼들이었군|1 +사망교회|1 +부끄러움은 국민몫이노ㅋㅋ|0 +이거 좋네요|0 +너무 사이즈컸냐?ㅋㅋㅋ미안하노|0 +시장은 자정 작용을 한다. 보람튜브가 저 돈 주고 일 시켜봤는데 만족할만한 퀄리티가 안나오면 돈 더줘서 잘하는놈 알아서 뽑을거야 걱정하지마저 돈주고 일시켜봤는데 저 정도급의 편집자를 써도 되는 난이도의 컨텐츠들이거나 자기들의 사업에 충분히 가용 가능한 범위니까 알아서 운영하는거야.|0 +취소까지 가즈아|0 +아 저도 속았네요 ㅋㅋㅋ|0 +성립하겠냐?|0 +?? 대체 뭘당햇다는거임 니가 당한거아님?ㅋㅋ|0 +모르면 배우고가라 병신아이슬람 역사 상식선에서 다 아는거라고? ㅈㄹ마 병신아그럼 니가 팔레비왕조에 대한 의견을 설파해보던가 ㅅㄲ야|1 +애잘낳것다|0 +05게이인데.해빔소스 못무봐따...|1 +이쁜 김치녀인거 빼고는 상대가 안됨 ㅋㅋ현우진 저사람 스탠퍼드 수학과 ㅋㅋㅋㅋㅋ|1 + 뭐 현장출동직도 존나 큰 화제 없으면 족구나 차는 개꿀직이고 |1 +다들 속으로 똑같이 생각하면서 아닌척 정의감에 불타느척 해야 사회적 으로 형성된 공감대에 어울린다 생각하는거지 ㅋㅋㅋㅋ 위선자 새끼들 지 새끼 용접공 시킬려고 하면 기를 쓰고 반대할 새끼들이 누구보다 용접공을 무시하지만 그렇지 않은척|1 + 그러니 이 정부가 서울 집값을 잡을 것이라는 망상 따위는 버려야 한다. 지지층이 집가진 사람이고 그들이 집값이 올라 더욱 지지율이 올라갈텐데 그걸 왜 포기한단 말인가. 서민정부는 그저 코스프레만으로 충분하다.|0 +쟤 누가뽑았냐|0 +니 수준 드러내지 말고 |1 +일본 망할때까지 주욱 장기집권 가즈아|0 +펭귄떼가 줄줄이 입수하는거랑 같다고 보면된다.|0 +팩트 ㅇㅂ|0 +좌파는 그냥 북한 보내서 김정은 충성하며 '인민'으로 사는게 딱 맞는 수준이다.|1 +오히려 신해식을 내쫒고 대화를 거부하더라|0 +의료수준은 예전부터 높았는데 제대로된 정부가 있으니 수준높은 의료시스템이 원활하게 작동되는겁니다.|0 +나도 그 생각함. 현대 알바 아닌가?|0 +??? 궁뎅이 자극으로 최고라고하던데|0 + 군대 가기 전에 군대 존나 두렵잖아|1 +한달간 진짜 밥먹고 총쏘고 밥먹고 총쏘고 |0 +웹사이트 접속만으로 아이디 비밀번호 유출 될 수 있다는 기사 내용 유럽의 특정 웹사이트들에 접속할 경우 아이디와 비밀번호가 유출된다. 또 다른 변종은 제목이 ‘Facebook updated account agreement’, ‘new account agreement’, ‘new Facebook account agreement’, ‘updated account agreement / 웹사이트 접속만으로 랜섬웨어에 감염 될 수 있다는 기사 내용 이번에 유포된 ‘랜섬웨어’ 버전은 해당 사이트에 접속만해도 감염될 가능성이 높아 피해가 클 것으로 전망되고 있다.|0 +말한놈이 쓰래기네 친구가 평생같이살 마누라를 모욕했으니|1 +한국에서 살았던건 유학생활 할때가 끝이었어|0 +루시드드림으로 도망쳐서 행복하게 살면 안되는데|0 +상어 진짜 좆같다 상어는 다 뒤졌으면|1 + 선거며|0 +이만희 한티 가서 따져라 ㅋㅋㅋ|0 +대단해 보인다?? 니생각 ㄴㄴ현직 개발자 앉아서 8000~10000달러 번다리플 ㄴㄴ|0 +생체실험이 아니고 중공군과 교전중 부상당한 일본군을 치료하는 장면의 사진 등임|0 +근데 한편 찍어서 여러편으로 편집해서 내는 경우도 많지 않음?|0 +좀 오바인듯 나는 저정도로 잘생기지않았는데도 걍 둘이서잇으면 저렇게본다 잘생기고자시고 그냥 사람으로써 괜찮으면 아이컨택하면서 얘기나눔;;|0 +박보검 vs 서강준 |0 +돈도벌고 명예돈얻겠다고?|0 +존나비싸네 어쩌네 투덜대도 결국 너도나도 사재끼죠? ㅋㅋ |1 +많이 벌어도 사업 확장에 돈 다 쓴거 같은데.|0 +아센시오 누워있으면서 피파만했나|0 +토목기사따면 먹고살만하냐|0 +우좀 병신들은 대가리를 깨야해이런 새끼들이 정치질하는거보면 어디 금붕어새끼들 키보드치는 줄 알자너ㅋ|1 +나뻤당 ㅋㅋㅋ|0 +이게 모든 인간들이 같ㄱㄴ한데|0 +침 튄거라 ㅁㅈㅎ|0 +니가 늑대냐? 왜 울프(울부)짖어|1 +ㅋㅋㅋㅋㅋㅋㅋㅋ|0 +저 ㄱㅅㄲ 난 길가다 만나면 즉각 ㄷㄱㄹㅇㄷㄹㅉㅇㅂㄹㄷㅈㅉㄹ|1 +왜 얼탱이가 없어 광주도 똑같이 이렇게 시민구단 창단했어 상무가 그런 역할임|0 +제발 부탁인데 외국여자랑 해..매력적인 스시녀가 있는데 왜 자꾸 한녀에 빠지냐. 한녀들이 스시남 좋아하는거 봤냐? 빨리 스시녀 대량 입국 허용해라..|1 +처리를 잘해서 조용한가? ㅋㅋ|0 +18번에서 들켰네 ㅋㅋ|0 +광화문 일게이들 진지하노ㅋㅋㅋㅋ|1 + 무거운게 대충 무게가 쌀한가마니 정도 자주들어야되냐 노가다가면?ㅎ|0 +광주 전남.. 열차타고 .. 김대중 가는날..|0 +지랄하네 그나마 유의미한 연구결과가 키랑 비례한다는건데 해외 조사에서 세계 각국에서 몇명 표본 모아서 진행하니까 한국은 13cm더만 평균|1 +막년도 비례 출신|0 +수능영어에서 문법강의 2개밖에 없는거 알지?그래서 대부분 구문독해 강의 많이 듣는데김기훈 천일문 인강 들은 애들은 병신이라서 그거 들었냐? ㅋㅋㅋEBS에서 그래머 존 종합편으로 강의한건 정말 좋지만메가시절에는 영문법 말고는 별로였다.오히려 독해는 김기훈, 김우철, 최원규 이 분들이 괜찮았지.|1 +저런 프로에 뭐하러 출연하는거냐 출연료 몇 억 주는것도 아닐테고 저렇게 자기 인생을 다 까발리는게 무슨 득이 있어|0 + 전여친 158 이었는데 여자든 남자든 키 큰게 좋음|0 +그리고 뼈 하나 더때리면 여론조사 별거 아니라고 우기는 틀많은데 이석기경기연합 어쩌고 그것도 여론조사 조작 결국 정당해산까지 간거임 선거는 민주주의꽃이라서 엄격함 닭이 여왕마인드 조선시대머리라 막한거지|1 +니네집은 얼만데?|0 +동의완료!!|0 +거 너무한거아니오?|0 +폼 다 떨어졌다 생각했는데 어느순간 딱 버텨주는거보고 클래스는 다르구나라는걸 느낌;; 시즌중반 미드진 붕괴때 프레드 각성 & 마티치 부활아니였음 챔스경쟁 꿈도 못 꿨을듯|0 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +도산대로에서 람보르기니 요란하게 끄는 부류인갑네|0 +추천이요~|0 +이집트 피라미드 만들면서 태권도 품세를 한 기록이 있다!!!|0 +아니 수천억 가져야 버는돈을 박가린 존못 bj 가 비슷하게범 ㅋㅋ 20프로 슈수료 떼기전 18억 ㅆㅂ 이해가 가냐? 나이도 존나 많은년인데 누가 쏘는거야?|1 +맞음ㅋㅋㅋㅋ|0 +기술쪽은 공부 안하는 줄 아나보네.. ㅉㅉ설마 기술 배우는거랑 단순 공장 조립, 노가다랑 같다고 보는 건 아니지?|1 +정말 쓰레기같은 동생이다|1 +너 닉네임에서 불향기가 느껴진다 ㅇ ㅇ 특수용접이니 그거 고수익 이라면서 나도 ㅅㅂ 그런거 하는 사람 있으면 몇년 따까리 보조로 월 150만원 받아도 되니깐 좀 배우고싶다기사 찾아보니깐 중년의 나이에 도전한 어떤 아줌마들 한달에 천따리 이상 버는 것 같은데 대체 그런건 어디서 배울 수 있노?|1 +갠지스강치고 깨끗한데|0 +대구 ㅁㅈㅎ|0 +ㅉㅉㅉㅉㅉㅉㅉ|1 +쥬지 승|0 + 왜 말을 못해 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +와 시발난 작년에 30됫는데 좆소에서 나랑 1살위 새끼한테 20대초반 후임갈구는거 마냥 진짜 날 모자른새끼처럼 갈궈서 빡치고 퇴사하고 1년가까이 히키생활중인데 시발 니글보니까 진짜 생각할수록 좆같다빡대가리에 남눈치 존나보고 피해의식쩔고 나이는 똥꾸멍으로 보고 사람들은 첫인상부터 날 만만히보고 쉬펄..친구도 찐따밖에없도 모쏠아다 일게이라서 씹쫒같노 평생 이렇게 비참하게 살다 뒤지겟다이기|1 +킬러조|0 +디지기전 이름은 카라스마 렌야|0 +박나래보다나은거 ㅇㅈ|0 +흔들어라 흔들어라 이기|0 +코다리찜부터 퇴출해라 시발 비린내에 씹으면 고무줄 씹는느낌 좆같다짜장밥 나오면 썩은만두에 고무줄잡채도 좆같고사골곰탕 나오면 깡통한개에 고기 2~3점 나오는 마법의 국물도 좆같고|1 +댓글로 페미한테 동조하는새끼들 보이노 역겹네보겸을 왜 까겠냐|1 +아... 내 감동 물어내|0 + 난 윈7에서 끊긴다기보단 로딩이 좀 길었는데|0 +그때 주택 현관위가 장독대 놓는데임|0 +나루토|0 +얼굴안보여서 ㅁㅈㅎ|0 +빨ㅇ줄게|0 +이거 아직도 당하는 븅신들이 있다는게 더신기|1 +20대중반이면 아직시간만노 기왕가는김에 첫직장 좋은곳으로가라이기|0 +양준일은 나이도 차고 경력도 없으니 그럴만 하지.|0 +가오잡을 때 특징이라기 보다는 키 작으면 괜히 가오를 잡는 무리수를 함|0 +저런거 온게임넷 에서 스타명승부 틀어주듯|0 +귀천있지 ㅎㅎ도태되고 게으르고 맨날처놀면 허드렛일하는거고머리좋고 공부잘하면 나랏일하는거고적당히 자격증따면 중간가는거고|0 +전라도상이네|0 +ㅈㄹ하노 ㅋㅋ 바로 눈앞에 모니터도 32인치인데 존나 작아보임|1 +좋은 사진에 좋은 조언까지 완벽하다|0 + 여자는 진짜 인생 외모순이라고 봐도 과언 아니지|0 +옛날에 힛갤간 정태준이라고 있었는데 걔는 정장입고 자전거타고 24시간만에 서울에서 부산가던데|0 +시체닦으며 시체랑 대화나누고 싶다|0 +솔직히 용접공 인식 안좋아진거 쟤 영향이 큰 듯|0 ++ 성북구 장위동|0 +그립습니다.. 당신의 진심을 알지 못하고 떠나보낸 후에야 당신의 소중함을 알게 됬습니다.|0 +국가기관 시스템을 맘대로 쓰고 안쓰고 했습니다|0 +니에미 자궁 방사능 오염됐다는 말이있던데|1 +인민감시 시스템 구축을 위해서 안면인식기술 데이터로 쓰려고 중국공산당이 큰그림 그린거임|0 +ㅋㅋㅋㅋㅋㅋ여기서도 빨갱이김정은이라고 안한다고병신앜ㅋㅋㅋ 알고쪽팔려서이러는건지 이런말도 이해를못하는건지 구분안갈정도네|1 +ㅈㅈㅂ|0 +저때 간사람이 10명이다 치면 지금 다 성공했어 내가 어떻게 성공했나 한국공기안마시고 좌빨개새끼들안봐서스트레스안받고 개중공조선족이나몰상식한인간들테러안당해서 좋고이게 내 지론이다 |1 +멸종을 안하는 이유|0 +26만중 최소 1/3은 일베출신이라 확신함|0 +ㅠㅠ나왜그렇게칭찬해..|0 +ㅅㅂ ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +아니 김씨도 그냥 컨셉이지 실제 산업역군이라고 생각하는 병신이 어딨냐일하러 갈때마다 인간 쓰레기 취급 받고 구석자리 아무대나 오줌싸고 함바집에서 거지같은 밥이나 먹고나서 햇볕잘드는곳에 앉아있으면 머리가 멍해지는게 매일 매일 일상인데|1 +용자네|0 +뭘 끼워맞쳐 ㅉ 티모테오 내가 처죽여냐 ?? 지가 처맞아죽었고 그게 기록상 팩트쟌아 그런데 뭘끼어맞쳐다는건지 시비걸지마라http://maria.catholic.or.kr/sa_ho/list/list.asp?menugubun=saint&today=&today_tmp=&ctxtCommand=&ctxtLogOn=&ctxtSexcode=&ctxtChukday=&ctxtGaladay=&Orggubun=101&ctxtHigh=&ctxtLow=&ctxtChecked=Checked&oldrow=&curpage=1&ctxtOrder=name1%2Cgaladaym%2Cgaladayd&ctxtOrderType=&ctxtSaintId=&ctxtSCode=&ctxtSearchNm=%EB%94%94%EB%AA%A8%ED%85%8C%EC%98%A4&ctxtChukmm=&ctxtChukdd=&ctxtPosition=&ctxtCity=&PSIZE=20|1 +오십넘으면 내근으로 많이 돌린다|0 + 중대 입결표 뒤져봐~ 이러고|0 +친일앞잡이매국당 놈들이지머.|1 + 휴거는 2021년 3월경으로 예상 하고 있는데 |0 +헬스목적이 순수 운동이 90%이상이라면 시설좋은 헬스장은 절대가면ㅁ안됨여자들땜에|0 +내친구가 tv에 몇번 깔짝대다가 사라진 무명 연예인이엇는데 바에서 알바하다가 결국 연예인 포기하고 호빠됨|0 +근데 신기한게 쟤는 그냥 성괴에 이쁜거도 아니고 짱깨놈 대리고 사는게 산박하다고 생각해서 그런건가? 왜 티비에서 밀러주는 이유가 뭐임? 짤만 봐서 그런건가 이해가 안가노|1 +아들~ 한건 더하면 아버지 완전 뜨게 할수있어!!|0 +zz z z z z 나랑 용접배울래?.?|0 + 제대로 하려면 "미치고 즐겨야한다"|0 +인간!.. 날 키워라...|0 +그리고 걔네들 소득들어오면 연금깎여서 일 하지도않는다|0 +으따 성님의 오줌이랑 똥도 전부 일제꺼랑께요?!? 성님 눈코입도 전부 일제꺼랑께요?!?|1 + 자기들 밥줄인데?|0 +??? : 축하드립니다! [뉴트리아 슬레이어] 칭호을(를) 획득하셨습니다. 이제 다른 유저에게 칭호가 노출되게 됩니다.|0 +응 알겠으니까 꺼져|1 +좌빨새끼들은 내로남불 우기는게 답인줄 아니까 인간 이하새끼들|1 +저런건 떠벌여야지~~~|1 +걱정마라 내 고추 튼튼해|1 +이탈리아 상원의원이 말했죠|0 +ㄹㅇ 명령을 그렇게 칼같이 내야 공권력이 살고 아가리 닥치고있지노무현때 촛불들었으면 애새끼들 다 화형당했을듯|1 +철학=종교|0 +@봉봉주세효 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +네~ 이젠 의미 확장 시도하고 있죠아주 그냥 뭐하나 걸리면 쥐잡듯이들 잡는구만 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 + 그래, 돈문제일 뿐이라고 치자. 그렇다면 공공복지를 위해 의료인들의 연봉은 현저히 낮춰져야 한다는게 그러한 배경에서 도출될 수 있는 유일한 논리적 답변일 거다. |0 +저 발언은 2013년도에 나온 발언이다.|0 +큰 의료비 나갈때가 좀 있나?|0 +그럼 수능도주작이고 공무원도주작이면 대체뭘하냐ㅋㅋ|0 +1. 핵융합|0 +암울한 뉴스노|0 +가르치는거랑 아는건 별개 영역이다|0 +앞차 블박보고 이미사고난거였으면 니 말이 맞는데 지금 증거로는 내말이 맞다|0 +전두환 대통령때 가장 나라가 정의롭게 돌아갔다. 악을 제압했고 선을 보호했다. 그래서 국민들 다수가 행복할 수 있었다.|0 +게이 몇살이노|1 +못할줄 알고 아주 선심쓰듯 말했는데 체결 되버렸네. 어떤 표정일지 면상 좀 보고잡네.|0 +네이버 =틀딱놀이터 다음 = 좀비놀이터 |1 +진짜 일본은 저렇게 이쁜데 중국은 씹창난 이유가 뭐냐?같은 동양인데..유럽은 각 나라들이 다 개성있고 멋있잔아한국도 외국인이 바라보기에 저렇게 아름다운 나라일까?|1 +공산주의도 아니지 그냥 망상이지이상 (선동할때)다 같이 잘 먹고 잘 살자현실다 같이 평등하게 대가리 깨지자 (기득권 소수 제외)예) 중국, 북한|1 +닭날개구이 개맛있음 ㅋㅋㅋㅋ|0 +자한당 전체보다 훨낫다니까 ㅋㅋㅋ 중권이형 화이팅!!! |0 +수시합격하고 심심해서 수능보러왔다가 2교시까지 찍고 도시락먹고 집에가는 애들이 맞는게 7등급 아니냐ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +굵어죽거나 오염되 죽거나..|0 +이사한번하면 몇년치가 쌓여버림|0 +그람시롱 투표할 때는 아따 우덜 편을 우덜이 안찍으면 누굴 찍는 당가? 하면 민주당찍.|0 +인생 곱창나는 최단기 루트탔네|0 + 주로 은행쪽 전문인데 은행이 줄어들고 있어서 일감 많이 떨어짐.|0 +얼라이언스를 위하여|0 +헤어 누드도 없냐 ?|0 +그냥 상대하지마|0 +니 말대로 무작정 은행 대출이 있다고 |0 +네다음 동부산 틀딱|1 + 아몰랑...|0 +병신지잡대년 군대로꺼져|1 +어디 회장이 걍재미삼아하겠지ㅅㅂ|1 +해피바스 바디로션보다 좋냐 ??|0 +전라도 사랑은 언제까지?|0 +병신새끼...|1 +틀딱 들 이거보고 서냐?|1 +니같은거 자살하면 ㄹㅇ 아무도동정안해줌 ㅋㅋㅋ|1 +유명한 곱창집 알바해봤는데 진짜 손질 존나 힘들다 곱창은 인건비가 70%라 생각하면됨 제대로 하는 집은 손질이랑 숙성만 이삼일 걸린다 보면됨|0 +지랄 저거 수업 듣는 학생들 학부모 중에도 용접하는 사람들 있을텐데, 대가리 나빠서 용접한다는 소리 들으면 참 기분좋겠다|1 +윤석렬 장모 비리 덮으려고 미리 준비해서 터트렸나?|0 +너도 삽자루세대노|0 +부럽다 새꺄|1 +280원 ㅇㅂ|0 +리뷰 이천개 이상 달릴동안 4.8 유지해봤는데저런새끼들은 히스토리 들어가보면 어떤 가게든지 다 불만 투성이임다 버린데 개 병신같은 새끼 ㅋㅋㅋ병신 보존의 법칙이라 생각한다 물론 대응은 잘 해야지|1 +투신자살 하는 사람들이 정말 어찌보면 대단함. 번지점프 한번 해본적있는데 나름 한국에서 꽤 높은 수준이었음. 근데 안죽을거 알고 밑에 물도 있고 한거 아는데도 정말 뛰기가 힘들더라. 참고로 나는 어느 놀이공원을 가든 단 한번도 무서워서 못탄적 없고 늘 다 더 무서웠으면싶어하는 강심장임. 근데 그런 나도 번지점프는 진짜 개쫄리더라. 놀이기구랑 다르게 내 스스로 액션을 시도해야하는 점도 그렇고, 단 한번도 살면서 경험해보지 못한거라서 그런듯. 어디 높은데서 낮은데로 뛸때 바닥을 보고 늘 뛰어왔는데 바닥이 보이지 않는 공중, 허공으로 뛴다는게 진짜 내 뇌가 한번도 경험못해본거라 진짜 엄두가 안나더라. 걍 발이 안 떨어지는 느낌.... 한 5분 망설이다 뛰긴했는데 다시 하라그래도 진짜 개쫄릴거같음. 안죽을거 알고 뛰는것도 이래 힘든데 자기 몸이 산산조각 날거 알면서 뛰는 사람들은 와.... |0 +제발 다구속되길|0 +?? 넌 어떻게 그런 머리로 5랩을 달성한거야|1 + 그이상 받는건 불공평한거다|0 +야소 충때문에 이미지가 이렇지 사실 성능만 보면 사기챔임|0 + 이봉창을 진실 그대로만 알려도|0 +30 넘고 클럽가지 않냐....혼자 시끄럽고 어두운데서 독한 술 마시고 헤롱대는거 좋아해서 시간나면 간다...걍 혼자 술만 드립다 마심..그러다 토하고 다시 마시고..|0 +좆같은걸좆같다말도못함?|1 +인민재판 익숙해야지|0 +야구하시네요|0 +개드립이지? 댓글 1페이지에 오타라고 써놨는데 으휴|1 +육고기는 맛있워....|0 +75면 45평대 거실에나 놔야지 밑도끝도없이 큰거사면 별로다근데 티비가 진짜 마니 싸지긴햇음|0 +저정도 응디는 운동안해도 가능하다 시발 보정레깅스쳐입엇는데 저정도는 다 나와|1 +그런 한남들이랑은 근본부터가 다름|1 +짱개 스파이 새끼들 목을 쳐야됨|1 + 머한민국이 북중러일미한테 조리돌림 당하니까 만만하게 보고|0 +새차 준다고 하니까 저 ㅈㄹ 하지 ㅅㅂ세기|1 +일단은 전라도 |0 +ㄴㄷㅎ~|0 +1. 경기중에 선수가 자신의 본 포지션이 아닌 다른 포지션으로 이동해서 공간을 창출하는 행위공간창조 공간창출 공간침투 침투창조 이런식으로 부를수 있을거같고2. 한두 선수가 서로의 위치를 바꿔서 상대 수비를 헷갈리게 하는 행위위변교란3. 팀전체가 공격과 수비 일정 상황마다 포메이션 자체를 바꾸는 행위공수비통일전술 공수비대칭전술ㅈㅅ 개오바해봣슴 뭔가 머릿속에 생각은 나는데 표현할방법이없네|0 +그래서 제가 댓글 잘 안답니다 티날까봐|0 +육덕 오타쿠 백마.jpg|1 +지금 몇살인데?|0 +긔/??워마드임?????|0 +야동도 야동이지만 게임이나 애니 같은 것도 빠질 수 없음진짜 일본 문화 한국인들 싹 못하게 막으면 다시 하게 해달라고지랄난리부르스 떨듯즐길건 다 즐기면서 일본 ㅈㄴ 욕함 ㅋㅋ 루리웹 뺨치는 이중성임 |1 +불어할줄 모르면 살기빡셈불어못하고 추위에 약하면 지옥일듯 ㅋㅋㅋ|0 +20군번중에 쟤들이 최고참이다 이기|0 +왜? 꼭 필요한 일이야? ㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +부럽네요|0 +직업군인이셨다. 해군 하사관|0 +시노다 아유미 ㄹㅇ 몸매때문에 좋아했는데 은퇴 후에 아무런 흔적이 없어서 아쉬움|0 +내친구도 용접공인데 보호장비 드립치면서 괜찮다 하던데 무용지물임?|0 +그 새끼들이 부패하면 누가 견제해???|1 +요즘 170대 많더랑 ㅇㅅㅇ|0 +이것들이어디서ㅋㅋ|0 +게이는 무슨 일하노|1 +그냥 서양에서 전해오는 빨간머리 전설같은 것을 말하는 것이냐?|0 + 공산주의는 망해서 없어진것임|0 +누나 내 쥬예즤 주겅..|1 +그래야 본인이 평범해지니까. |0 +ㅈㄹ ㄴㄴ|1 +아 진짜 개새들 뒤통수 졸라 쌔게 까고싶다~~!!!!!!!!!!!ㅅㅂ|1 +부들부들 떨리냐?ㅋㅋ|1 +일특원 ㅇㅂㄹ|0 +인터넷강의특별법 제정됐을듯|0 + 하여간 더럽게 역겨움.|1 +호로씹종자 새끼들이네|1 +시발 존나 잘함|1 +모임, 집회 하지말라면 좀 하지마|0 +사드 ㄷㄷㄷㄷㄷㄷㄷ|0 +모쏠아다 29년차인데아직 희망있으니 더 밀어붙여라 진심이다|0 +얘들은 그냥 좀 사는 북한임|0 +ㅈㅈㅂ ㅁㅈㅎ 25렙게이가이걸?|0 +전자발찌도 차야 함...|0 +타이항공에서 비행기 연착되어서 제공했줬던 숙소보다 훨씬 좋네요|0 +느꼈나 봅니다|0 +현금으로 2000억을 가지고 있는 개인이 있댜고?|0 +은교입니다.|0 +다음날 아침에 뉴스보고 놀란기억이...|0 +어딘데그리싸노|0 +김주하 이런사람이 앵커를 맡는다는게 아이러니 이자 한장의사진에 어울린다 도대체 왜?? 이딴 앵커가 ??|0 +씹쿵쾅 맷돼지들|1 +그래도 찍어요|0 +그런데니까 너가 들어간듯|0 +음 이게 맞다|0 +장모랑 이건희도 윤석열 손절하겠는데|0 + 민원인 없으니깐 지들끼리 자리지키고 농담따먹기 하고 있는데 하루하루가 치열한 대기업하고 비할소냐.|0 +최고 180까지 밟아봤음(이때 딱 한번 처음이자 마지막 이였음 아반데로는 2~3초 정도)|0 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ |0 +어쩌다 우리 아재됫을까|0 +그럼 나 한 번 연주해 봐|0 +일베와의 전쟁을 선포하겠습니다|0 +미친새끼들 저거 사실상 외압이네 .. 청와대에서 공문보내면 불법아니냐?? ㄷㄷㄷㄷㄷㄷ|1 +ㅉㅉ|1 +짱깨문베충이나 잡어.|1 +서지영 나랑 사귐|0 + 몇달이상 들고있었어야 한다 이런 조건 있냐?|0 +아이린은 빼 씨벌년아 뒤질려고 일베 제외한 모든 곳에서 여돌 원탑인정하는게 아이린이다|1 +어이가옶노씨발ㅋㅋㅋㅋㅋㅋㅋ|1 + 일본처럼 7인의 사무라이 나오고 할복하고 죽으라는것도 아니고 티케이 불출마 한명도 없는데 지지율 올라가길 바라로 총선 이기길 바라고|0 +일베놈들 이참에 박멸가즈아~~~|1 +앞으로 머해먹고 살아야 할지...|0 +상식적으로 유튜브 가지고있고 구글맵스며 뭐며 정보화시대에서 유리한 고지는 다 가진 구글이 망할 일은 없지않냐? 해킹당해서 개인정보유출 이런거되지않는이상|0 +머야 먼소리냐|0 +해외 나가믄 죄다 토요타고 흉기는 찾아 보기도 힘든데... 방구석 주모 믿으면 발등 찍힌데이|0 +상대적인거지 뭐|0 +영어도 제대로 못하는 칭챙총을 굳이 쓸려나 아무리 호주라지만 |0 +어차피 안될 거 알면서 공약하는 거 보면 정의당 가면 될 듯|0 +지금도 필리핀보다 못산다 거지 국가임|0 +그렇게 해도되지|0 +ㄷㄷㄷ 이래도 간첩이 없다고??|0 +대리 운전당시는 최소 성인 2명이 타고 있어 차체가 내려가 있었지만 아슬아슬하게 스토퍼에 걸리지 않을 정도였을 수도 있는데 반대로|0 +돼지는 눈물 안흘리냐?|0 +천주교는 하느님의 개념이 좀 더 넓은 편이라, 외계인이 있거나|0 +엄선 ㅇㅂ|0 +무식하면 용감하다는 말을 몸소 실천해주는 섬숭이들ㅋㅋ|1 +폭동은 진압해야지 병신 새끼야|1 +에미 씹 ㅋㅋㅋㅋㅋㅋ 마인크래프트로 생각하네 인생을|1 +전시에 헬기가 얼마나 위력적이길래 도움이되노? |0 + 먹고 먹히는세계가 선의 세계일리가 없잖아?|0 +드립백 좋지요|0 +웹툰작가인가? 뭔 개 좆도 아닌소리를...|1 +저 30프로에|0 +글자를 귀로 듣는 저능아 수준 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅎㄹ|1 +?공수처 통과 될거도 알고 있었냐? 다 통과 되고 있는 마당에 대체 어딜 봐서 파멸이 보이노??|0 + 부착연결하는 손잡이 추가 구성하면 셀프컷 졸라 쉬워질거다|1 +네이버 답없음 야후 롤백해야될판|0 +경영 생각하면 고기값 싼 고기라도 따로 더 구워서|0 +어떤 말로 위로를 드려야 할지 ㅠㅠ|0 +중고가 미쳤던ㄷ니ㅣ|0 +ㅇㅋ ㄱㅅㄱㅅ 평생 담배만 열라게 피다가 액상형 전담하면서 담배는 아예 안피게 됐는데 문재앙 때문에 좆같네..|1 +근데 이런 상황인거 이국종도 알고있던데본인 책 보니까|0 +ㅇㅇ 이제 끝임|0 +진짜 김씨저장소답게 엄청 발끈하는구나 근데7등급이면 호주에서 영어 스피킹은 가능하냐?|0 +간호사코스프레한다고 간호사비하한다고 기사나오는판에 용접공 노가다 힘들하는사람들이 웃음거리로 만들어서는 안된다 이거 그냥 넘어가면 용접공이하 현장직들은 동물원 원숭이로 전락하는거|0 +돈사랑이요^^|0 +맨첫짤 스무살때 진짜 좋아했던 짤인데 햐..|0 +저런 ㅅㄲ는 아싸든 히키든 인생에 전혀 지장없겠노물론 주변에서 물고빨고 ㅈㄹ나겠지만|1 +그리고 몇 년 후 민좆당 노동자 대표 청년 비례대표로 등장 테크트리 ㅋ|1 +게이야 왜 근거 못 대누?|1 +아 소름돋네...ㅋㅋ|0 +여군이 간부만 있어서 그럴 수도 있을 것 같습니다.|0 +집 그리 좋아보이진않는데 장인이 일도 못하게 한다고? 얼마정도ᄇᆞ는데 장인이|0 +강사면 강사 답게 우선 5등급 목표로 하고 올려라 천천히 목표를 높여라 해야 강사다 강사가 왜 강사겠냐 |0 +우리나라에는 고유정같은 년들때메 안됨|1 +뭔 시발 병신같이 월권이야 일게 시장이|1 +내용과 상관없이 저 짤은 아이돌처럼 나왔네|0 +개세이들|1 +응 그거 말고 전기차 요금제가 따로있음. 경부하시간대에 절반 이하 가격임.|0 +ㄸㄷㄸㄷ|0 +코로나19 같은 인간.. ㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +짝짝짝. 시방새는 별로|1 +성령이 미만 잡|0 +어디 가둬놓고 불태워죽여버렀음|1 +가서 뒤지는새끼들은 성매매 마약 돈 관련된애들이 뒤지는거다 도둑 강도들많은 터미널역부근빼고는 쟤네도 일반 관광객들은안건드려 그리고 뒤지거나 처맞는애들대부분이 필리핀애들 존나 무시해서 그런경우가많음쟤네가 비록 못살고 한국인보다 열등하고 똥송해도 자존심센애들많다못배운 한국새끼들이 가서 지가 뭐좀 되는줄알고 필리핀애들 무시하고 종부리듯이대하니까 역으로 쟤네도 이제 한국애들 무시하고 만만하게보는거임|1 +한국은 좆같은 황사가 날아오지만역살적으로 땅의 회복력이 올라감|1 +수통 뚜껑에다 박아! 오른 발 들고!!!|0 +멋을아는게이노 ㅋㅋㅋ|1 +별 생각 안 하고 썅년 하고 넘길 것 같은데 ㅋ|1 +ㅍㅌㅊ의 기준이?평균급으로는 솔직히 조올라게 노력하고 거기다 운좋아야될건데.. 한두번 하급여자만나는거면 몰라도..폭격?이병헌탑주진모장동건도 노력하고, 누구(일반인)는 10억을 바쳐도(별풍선) 까이는게 한국 남녀관계인디|1 +입이 좀 별론데|0 +정답은 강간|0 +역시 고상한척하면서 트위터에 야짤올리는건 뭔가|1 +사돈에 팔촌이어도 어차피 뒤돌면 남이다.|0 +에어닷? 그 이어폰 되게 작은거 있는데 그거랑 |0 +보수논객 10명 합친거보다 효과 ㅅㅌㅊ|0 +시발ㅋㅋㅋ 일이 점점 더 커지놐ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 대책 없이 일이 크게 번져가네|1 +그냥 한석원쌤 같은 리버스 투블럭 틀딱들만 보다가 얼굴 좀 예쁜 여자쌤이 가르치니 한남들 발정나서 보는거임|1 + 오늘은 좀 낫다|0 +용접이랑 영어 배워서 호주가면 진짜 사람답게는 산다돈 오지게 번다 한국보다는|1 +글을 쓴다는 사람이란 어때야 하는 지를 보여주네요|0 +근데 명의 집착하는 계집이랑은 대체 왜 사는 걸까?이해가 잘 안되네|0 +빵에서 이쁨받겠네|0 +ㅋㅋㅋㅋㅋㅋㅋ아 재밌겠다 ㅋㅋㅋㅋ|0 +그랜저랑 비교해보고 와라|0 +그게누군데|0 +이태까지 반일한 죗값치르는거지 뭐|0 +첫번째 이유가 ㄹㅇ 맞지특히나 뉴스나 기사 몇줄읽은 학생들끼리 대화해도 서로 존나 싸움 ㅋㅋㅋㅋㅋ 근데 둘다 ~~~아닌가? 이런식으로 밖에 대화못함기사 뉴스 제대로 읽어본것도 별로없고 그냥 비전문가니 뭐 이게 팩트인지 아닌지도 판단이 안되는 상황 ㅋ|1 +천원 이천원이 우습냐 찐따새끼야ㅋㅋㅋㅋ|1 +ㅋㅋㅋ 난 저경기 뉴욕가서 먹었는디원정마이애미는 애미가 갈려하면 싸대기 떄려도 무죄라더라|1 +???? 저게 뭔데 1700만원씩이나 투자하냐??|0 +여의도 살기 좋음?|0 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ표정|0 +선관위 일해라|0 + 쁘아까오 포프라묵 2006년도 정도가 인간이 할수있는정점이다 |0 +좋은데요 뭐... 뚜껑열린 미친 차주분 꼭 신고 성공하세요. 잠복도 하시고요.|0 +이렇게 다 타먹었는데 결혼지원금 30만원 짜리 있었는데|0 + 뭐해먹고 사노?|0 +비연예인은 어디서 만났을까?|0 +체온이 사람이랑 똑같다잖아|0 +저거 조작이잖냐드루킹 좆선족 짱깨들이 조작하는거임 추천수|1 +급이 비슷해야 뭐라고 하지그냥 측은하다 일본, 중국은...|0 +시봘 존나 웃기네 섹스리스 였는데 야동에 출현해서 섹스하는 거 보고 꼴려서 둘째 임신 ㅋㅋ|1 +이얗호응|0 + 또 여자같은 경우 외모 뛰어나면 본인은 능력없어도 능력남 만나서 50 이후 인생도 잘 건질 확률 높으니|0 +근데 현실이 이상적이라면 이미 공산주의가 성공했겠지|0 +개씨발 , 나라 개좆같네,다 디져라|1 +구라치지마라 수학 모르는 데 어케 전기기사까지 가 복소수,삼각함수, 미적분 다 공부하더만|0 +주위에하도 주식해서 털린사람이많아 시작도안함|0 +정작 나는 물사러가기귀찮아서 브리타정수기 내려먹음|0 +아 나베상 아리가또 ㅌㅋㅋ|0 +짱구 아버지 사립 명문대 와세다 출신|0 +푸헬헬|0 +시동 막 끈 차 본네트 안에 엔진 옆에서 잔다. 모르냐?|0 +그럼 세울려고 홍준표상상하는거야? 노땅취향의 게이였단 말이노.|1 +현실에서 사람취급 못 받고 낮아진 자존감,쌓인 열등감|0 +발랐으면 인성질은 기본이지.. 과거 비등했던 적이 무너져서 앙탈 못부리고 마음대로 할수 있게 되었는데 ㅎㅎㅎㅎ|1 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋ150000만원 이지랄|1 +그건 스펙때문이지 당연한 이야길 하노 게이야.|0 +앙헬 가고 싶다 빨통을 그냥 핥짝 핥짝 |1 +레이저 업체에 도면 보내면 그걸 가져와서 용접을 직접하기도 하고 SS41에 용접해서 만들어 오는게 얼마나 많은데..|0 +그생각이맞음11로 보이기도하고바지,신발 착장보기에 최적화됨차렷자세를 옆으로 돌리면 신발은 잘보이지만 옷은 안보이는이치랑 같음|0 +깜빡이 킨 차가 어딨어 븅신아 뚜벅이새끼 입좀 닥쳤으면 한다|1 +조선에서 살면되는데 왜|0 +버닝썬 사건이 엊그제 같은데 나라꼴이 망했구나|1 +난 그렇게 봐. 분탕들한테 놀아나서 부화뇌동하는 빡대가리도 분탕이라고|1 +그럼 일반영상,사진 파일명만 fc2 이딴거로 바꿔놓으면 걍 정지당함?|0 +ㅋㅋㅋㅋㅋ 인생수준ㅋㅋ|1 +굉장히 창의적이다|0 +정신은 멀쩡한데 눈만안떠지면 어떻할거임?|0 +과산화수소가 좋텐다.. 샤워좀 해~|0 +병신아 저거 유튜브로 재전송하면서 덧씌운 자막이잖아 자막 뒤에 방송사 워터마크 안보임?|1 +아라쪙|0 +멍멍 헥헥|0 +팩틍ㅂ|0 +아 착각을 ㅋ|0 +그건 기술자가 아니라 기사다 개병신아 제발 좀 ㅋㅋ|1 + 식인은 모든 나라가다한다 특히 홍어마을은 지금도 한다 아따 싸게싸게모이드라고 태잡았댜~(태>>어린아기) 으따오렌맨시롱 몸신혀쓰것구먼. 근디 오목이여불뚝이여? ( 암컷숫컷) 오목이랑께 아따.. 만나 겟고마잉. 근데하나여? 잉에잉 어느코빽지에붙이남? 아따 걱정 허덜말드라고 15년묵은 여우도잡앗씅께.. 워미...어디서구했냐? 아니아직도모르는 가베? 뭐슬? 니여우잡앗당께 ㅠ.ㅠ.. 아갑고마잉 나가아직 시식도못혓는 디.. |0 +김앤장 로펌 VS 우병우 1인.ㅅㄱ|0 +저건 아무것도 아님한가족 4명인가 공원 산책 나왔는데 하나같이 시커먼 소세지 패딩 쳐입고 잇드라소세지 패딩도 그렇고다 똑같이 시커먼 색 ㅡ,ㅡ;|1 +일간한국당|0 + 왜 제시를 못해 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +일베도 비슷하잖아 ㅋㅋ|0 +예전에 라뷰티코아 에서 한가인 보고 쓰러지는줄ㄷㄷㄷ여럿 밧지만 한가인이 실물갑!!!!!|0 +저렇게 아무렇지도 않게 살수 있을까?|0 +결혼안하면 돈아껴서퇴직후 남은생 놀고먹기가능 결혼하지마라|0 +이언주는 옷을 강남에서 따로 맞췄나 색깔이 다르네|0 +차라리 그냥 무소속을 찍어라|0 +ㄹㅇ ㅋㅋ|0 +그 새끼 호모 게이임|1 +그런짓 않함|0 +아니 김밥을 왜 묻냐니까 개 좆같은 븅신새꺄?|1 +역삼 교대쪽 다단계팔이들도 마찬가지다 대부분 국세청 소득통계 보면 쟤네들 월100만원도 못버는 병신들이90%이상이라고 보면됨 어설픈 세미정장입고 우르르 국밥집 편의점도시락 전전하는 카페충들 대부분 다단계 병신들이다 배운건 없고티비 연예인처럼 화려한 삶은 살고 싶고 무작정 서울올리와서빠져드는게 저딴 병신짓거리 ㅋㅋ이글 보는 다단계충 보험충은 무능력하면 공부를 하거나 몸쓰는 일해라개병신주제에 강남물 흐리지말고 ㅎㅎ|1 +사진에 가게 상호 넣었잖아|0 +도라에몽에 귀달고 눈 바꾸고 끝냈네|0 +개소리 하네 위장보수임|1 +그래도 안된다. 너희가 뿌린 것은 너희가 거둬라.|0 +삼보일배 도 하잖아요|0 +좆냥이주인쉑덜왜 집사집사 ㅇㅈㄹ함 ?|1 +삼전 임원 개많노|0 +이국 말고 러시아에 돌고래 수용소로 이감~~|1 + 나라 골라서 이민 가게|0 +진짜 우리 색시랑 똑같습니다 ㅎ 동지 발견이네요.ㅣㅎㅎ 우리 더 열심히해서 건강하고 부자됩시다요..!!^^|0 +짤방 팩트폭력 ㅆㅅㅌㅊ ㅠㅠ|0 +내가 저 영상 봤는데, 그렇게 진지하게 조언하는 말이 아니었음.|0 +환율시세로살거면 은행가서사지 왜씨발중고나라에서 돈을사고자빠짐?|1 +고생했다 ㅇㅂ|0 +덕분에 좆중딩~20대 까지 클론 양ㅋ성ㅋ|1 +응 일베 개새끼^^|1 +멍청한 년|1 +신.. 먼신? 아 .. 그 신 .. !!!|0 +유시민과 관련된 구린것도 없애버리네 저 미친 시발 새키가|1 +기다려 보죠|0 +느그 할배 그러나보다 ㅋㅋ|1 +고양이 영물이네? ㅋㅋㅋㅋ|0 +구미냐?|0 +삼촌이랑 비밀동지할래?|0 +지금은 비록 보잘것 없을지 모르지만 목표를 잃지 않는다면 가치있는 인간이 될 수 있을거다|0 +저건 중국이 아니라 몽골임|0 +거기 조선인들 스탈린이 강제이주시켯고|0 +저걸 히죽거리며 얘기하노 씨발새끼|1 +개념 제대로 박힌 판사한테 양심도 없다고 욕이나 한바가지 듣길...|0 +난 23살 98년생부터 저런생각햇다는거부터가 대단하다고 보는데?|0 +병신 조선처럼 인터넷 검열/통제 안하는점도 있음|1 +대변이 대검을 통해나오니 대검은 똥구멍인가 변기인가?|0 +제발 대한민국 식민지 삼아주세요 ㅠㅠㅠㅠ|0 +똑같네 추천.|0 + 지금 너혼자 울분을 토해도 이미 김치국은 망하기 전까지는 그대로 있는거 퍼먹기로 정해졌다|1 +ㅇㅇ 동탄아지매 2명이랑 만나봤지 뇌가없는대신 이쁘다|1 +한다 나도 저새기ㅈㄴ싫어서|1 +503이 무능해서그럼|0 +당연한 결과|0 +가이드가 눈탱이를 쳤는지 말았는지 확인도 못하잖아?|0 +이분 일상에서 하고싶은말 전부 입밖에 내신답니다 ^오^|0 +최소형량 무기|0 +통수쳐서 300인가 받았는데,개꿀 어차피 볼일도 만날일도 없음 ㅇㅅㅇ|0 +이래야지|0 +456으로 끝낫어야할 영화 존나지겹노|1 +한걸레는 가난한 좃쭝똥일뿐입니다.|1 +이야~ 내가 그중 1명이다 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +진라면광고 ㄷㄷㄷ|0 +털이 저래 존나 많은데 추위 많이 타기는 따뜻한거 좋아하고 존나 게을러서 저럼. |0 +엉금엉금기어서가자악어떼가 나 온 다악어떼!|0 + http://www.ilbe.com/view/11224594279|0 +존나 징징대서 싫음|1 +내려서 존나 자연스럽게 트렁크에서 드라이타올꺼내면 10만베|0 +찢어벌라??? 벌라??? 딱 말투가 그짝이네 ㅋㅋㅋㅋㅋ|1 +그냥 분리해..|0 +삼국시대는 빨아도 됨|0 +아..안돼 빠져나가면 안돼!! 예지눈나 이번기회에 완전 묻어버려야한다구|0 +언더아머 르까프급이었던걸 이렇게까지 만들어줬는데 쥐뿔만큼주노|0 +그냥 사람 안들어오는 페이지 링크주소 하나 범인한테 텔레그램으로 보내서|0 +짱깨 새끼들 때문에 지금 전세계 경제가 초토화 인데 코로나 끝나고 어떻해 나오나 보자 썅것들|1 +돈으로 투표함바꿔치기하고 선관위 돈으로 매수하면그냥 민주당세상이네 뭣하러 선거하냐?|0 +가만보면 그룹내에서 존재감 제일 없는 새끼들이 꼭 저렇게 파토내더라|1 +ㅇㅇ 내말이그말임 결국 적게벌면 적게버는 대로 스트레스일거고 그들처럼 돈이 많아서 걱정 안해도 되는 상황이어도 다른쪽으로 스트레스가 있을거임 아이유 팬은 아니지만 어린나이부터 성공한 연예인치고 멘탈이 정말 강한사람같은데 그런 사람정도 되면 버텨내는거지|0 +아잰데 모름|0 +썅것들은 오래오래 살려두고....|1 +미친년이네 ㅋㅋㅋ 손봐라 ㅆㅂ 외모도 보이네 |1 +와...그래도 음성 들어보니 엄청 차분하신거 같네요...|0 +일반적으로 남자가 저런 상황에 처하는데정작 자기가 쳐해보니 ㅈ같다 이거지 저러고도평균 남자들 이해못하면 공감능력 제로라는 소리|1 +도시들 다 보여서|0 +신생아 티나는 아기는 밤낮없이 한두시간마다 계속울고|0 +뷔스트호프 아님 슌|0 +말도 안되는 개소리임. 여자가 하는 말 잘 들어보면 90%가 쓸데없는 잡소리임. 즉 위 짤 같은애 한테 분노 할 필요가 없다는 말임.|1 +얘는 진짜 줏어들은 소리로 십년전 이야기를 하노 |1 +똑 떨어져라 시벌련|1 + 만박할겤ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +설레발 칠만하지 3쿼터까진 무난하다가 4쿼터에 뉴욕닉스 40점을 넣을지 누가알았겠냐 ㅋㅋㅋㅋㅋㅋ|0 +아직도 씨를말려야할정도의 머리를 가지고 댓글다는분들이있네..|1 +구울들 일어날시간이노ㅋㅋㅋㅋㅋㅋ|0 +@반일반북 //|0 +다음 난쟁이님 들어오세요~|0 +반응은 어떠냐?|0 +여초년들 물타기에 넘어간새끼들이 몇몇보이네|1 +진짜 검사네 멋지다|0 +씽크로율 ㅋㅋ|0 +보잘알 ㅇㅈ ㅋ|1 +백인.황인.흑인 구분은 외모로 하는게 아니라고. 인류학이나 유전공학이 미발달했던 19,20세기에나 당장 눈에 드러나는 특징인 외모로 구분했던거지 핀란드,헝가리인도 황인에 속하는거 알면 기절하겟네 이친구 ㅋㅋㅋ|0 +너무하노|0 +인간자체에 환멸이 난다 씨발꺼 ㅋㅋㅋ|1 +ㅋㅋㅋㅋㅋㅋㅋ 진중권 진짜 드립력이 고급지다 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ진중권 수업 청강해보고싶음 ㅋㅋㅋㅋ|0 +여친먹는거 한 입 먹어밨는데 좀,,,,국물은 갠탆앗음|0 +부모님집 서초인데 전文: 16후文: 21 (현재)개꿀띠?|0 +ㅇㅇ 옛날처럼 깡패정국이 아닌이상 불체포특권도 없애야지.|0 +호주|0 + 이게 진실임|0 +존나귀여워...|1 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ아침부텈ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 + 후자는 좆같아서 자퇴. 공부 존나 빡세게 해서 재수능 네임드대 입학하는거|1 +고수들은 오히려 차분히 대응 중임. 빠질 수 있다면 오를 수도 있다는 이야기니까. 포트 정리하고 아무것도 안하더라. 지금같은 장에서는 버티는 것도 내공임|0 +국격이 어마어마하게 상승함|0 +모유수유.브라질리언왁싱|0 +저 재활용쓰레기통은 대구가아니라 홍대에 설치했어야지|0 +그럼 마리아 보지가 스타게이트였노?|1 +니가기자냐창작소설가냐.|0 +붕신새끼들ㅋㅋ 또결혼못해서 안달낫네 ㅋㅋㅋㅋ|1 +내 컨트리맨도 별이 다섯개!|0 +업체이름 이니셜좀|0 +70~80년대 한국복싱>일본복싱2000년대 일본복싱>한국복싱|0 +너도 위대해지고 싶으면 니애미 젖탱이나 주무르던지|1 +제사는 더 병신같은 유교에서 시키는건데|1 +돈은 누가내고? 피해보는 직원들은 누가 책임지고? 꼬우먼 지가 병원파서 적자보면서 치료해라 ㅉㅉ|1 +홍어자슥 ^^ 조용히 분탕치면 내가 가만히 놔둘거 같았노? 홍어야 아쉽지만 우리같은 엘리트들에게 딱걸렸노 이기야~ 너 신고됐으니 밴먹기전 남은시간동안 어디 마음껏 짖어보려무나 ㅎㅎ;;|1 +메이웨더는 순 깜둥이인데 잘생긴편이고목소리도 ㅈㄴ 멋짐자기 직업,벌이에 프라이드도 높은편이고무엇보다 엄청 노력하는 편이지그래서 멋있음|1 +애들 학교다닐땐 외식도 많이하고 한끼만 떼우자..개념으로 라면도 가끔 먹었는데 온 가족이 집에만 있으니 삼시세끼 꼬박하게 되네요.|0 +전부 러시아임?|0 +아이디 걸어라 씨발|1 +우파 성향 남자가 최하위 계층이노 ㅅㅂ ㅠ|1 +전라도가 많으면 동네물 나빠지는거 씹팩트인데 |0 +쟤가 누군지도 모른는데 아이디를 왜 바꾸노? 단 두줄 적었는데 팩폭쳐맞고 뜨끔 했는지 댓글을 두개나 처달며 장황하게 개소리지껄이는거 보니 아주 뇌내망상 제대로 걸린 홍어틀딱 이신듯 홍딱어르신 자꾸 홍들홍들 거리시면 심근경색으로 착한홍어 될수 있으셔요^^|1 + 지금은 3년반 동안 애기가 양육되는 시기야 |0 +정은이한테 꿈깨라고 전해라 ㅋㅋ 그럴일없다미국이 미쳤다고 우리 받아주노?|1 +그럼 대충 운띄워보세요 포항지열발전으로 인한 지진이라던가 cnk 사기친거같은것들 |0 +좆까|1 +그런개미들많음 배당만받고튈려는애들 근데 삼전은 배당일지나도 안빠짐|0 +조잘알|0 +TEL이나 동우 생각중 ㅋㅋㅋ|0 +앱등이새끼들 아닥하노?|1 +킹덤이 진짜 잘 나가는 곳은 미국임. 미국에서 진짜 잘나감. 순위는 낮아도 수익은 높을걸|0 +사법부가썩었다~!!!이거 완죤 좌빨씹새들하고 똑같네??진짜 허경영이 답이다|1 +민폐존나심하네ㅜ 와..|1 +하루종일 방구석에서 일베 VS 연돈 줄서면서 일베뭐가 더 나음?|0 +대체 뭐하는 곳인지도 모르겠음|0 +복싱에서 발쓰냐?|0 +시벌...ㅠ|1 +당근아니노|0 +저런 지 지지자 간수 안하는 새퀴도 욕쳐먹어도 싸.|1 +허니버터 2|0 +그게 너 잖아.|0 +이래도 찍어준다니 할 말이 없다.|0 +왓능가|0 +그게다 남부유럽 대표 감성충들인 이탈리아, 스페인애들이 이민 많이 가서 그래. 신대륙에 산업시설은 커녕 허허벌판 밖에 없는데 가서 노조질부터하고 분탕치고댕김|0 +바디는 노예계약 당함? 주급이 왜 그렇게나 짜? ㄷㄷ|0 +저 사람 완치 됐고 문제는 티그가 아닌 씨오투 용접사였는데 마스크 따위 일절 안 쓰고 하다가 저 지경 된거 ㅋㅋㅋ 요즘 마스크 조올라 좋지 짱깨발 문세먼지도 확실히 ㅁㅈㅎ시켜주고|1 +조선시대 노비는 재산목록으로 들어갔었다니까??어디에 사는 김대감은 노비를 인간으로 생각했어요....라는걸 말하는게 아니라제도적으로 노비는 인간 취급을 받지 못했다고....이해 안되냐??|0 +아니 야 재벌2세도아니고 학원강사따위를 누가 질투를하냐 ㅋㅋㅋ ㅋㅋㅋ 그것때문은아니고지금 안그래도 경기 어려워서 취업난인데7등급 은 용접공같은 노가다나 뛰어라 하면서 노가다 비하해봐라 ㅋㅋㅋㅋ걍 씨발 누구나 다 화나지 ㅋㅋㅋㅋㅋㅋ '지는스타강사니까 저딴말을 편하게도하네? 씨발년이?'이런말나오는거지걍 정내미가 뚝 떨어짐|1 +그렇게 나라가 더욱 암울한 시대에 접어들죠..|0 +요즘 보험사들 왜케 개소리를 잘한데요?|1 +ㄹㅇ 시발 그냥 "다른 진로 생각해 보세요"라고만 했어도 욕 안처 먹었지.|1 +http://www.ilbe.com/view/11226094274|0 +대통은 노무현처럼 자살해도 기소권 계속 유지하게끔 법을 바꿔야함 |0 + 같은 옷을 입어도 옷맵시부터가 다르다는걸 느낀딘|0 +루스츠에서 팬션한다익이야|0 +진심 2초만에 해석했다 참고로 영어 5등급|0 +영양제를 인공으로 만드는거 자체가 천연이 아니야|0 +일베벌레들은 일베인거 들키는 놈은 버림 ㅋㅋ|1 +비난의 댓글엔 어김없는 작성자의 티나는 비추 -1|0 +폴란드 남자 평균 키 172로 상당히 큰 편임 |0 +처음부터 개척영업을 하면서 자기 자신만의 시장을 만들어 가면 충분히 롱런이 가능한데 대신 초기 6개월 정도는 맨땅에 해딩해야 하고 실적이 없을 경우 갈굼을 견뎌야 함.|0 +니 수준이 딱 일베 표본이지|1 +토론에서 지면 현피를 뜨는 지능 수준 잘 알았다 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +남자였어?|0 +일본어는 네이티브정도는 아니지만 회화는 꽤나 할 수 있다고 자부함|0 +니가 조커해라|0 +매도실패 개빢침ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +빨렙들이 퍼오는 정치글들 제목 보기만 해도 역겨움|1 +몇년도부터 이렇게 해준거냐?|0 +이럴 수가..|0 +나도 고딩 때 여친이랑 헤어지고천일동안 들으며 울어버렸었는데한때 팬이었던거 후회 중ㅇㅂ|0 +상속포기각서 법원 제출하는거 있음 사망후에 일정기간내에 법원제출해야댐 기간내에 제출 안하면 바로 빛 다 배우자나 자식들한테 가버린다|0 +휴거는? 내가 알기로는|0 +대검찰청에서 일반인 대변도 해주나?? 골때리네|0 +그냥 돈 더보태서 보잉747 사자|0 +한가인....|0 +모자리크 해라 ㅛㅣ발|1 +노가다 일베충 새끼들 ㅂㄷㅂㄷ하는거 보소ㅋㅋㅋㅋㅋ그러니까 공부좀 하지그랬노? ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 + 학식충들 글 특|0 +니가 사는 건물, 조선 산업, 제조업 수출 산업, 자동차 산업 , 용접없이 가능함?|0 +존버하면 되지 ㅋㅋㅋㅋ|1 +수학 7등급 집단이 아닐까??주예지 쌤이 그랬어!!|0 +아..밑에 글도 구라다 저기 보신탕집안다|0 +후장에 자리 남을까? 생각하는건듯|0 +분당에서 젤 놀게 많긴 하지만|0 +탈김치는 개뿔 김치 성형녀구만 |1 +흐미 이게 얼마야|0 +칼쓰는애 고르는법일걸|0 +댓글 다 달아주노|0 +ㄹㅇ걍보쌈대자시켜먹음 ㅂㅅ들|1 +그 파도에 쓰레기 같은 부산물도 같이 떠밀려 오지만|1 + 북한 새끼들도 도망치는게 시베리아인데 ㅋㅋㅋㅋㅋ|1 +재는 그래도 돈좀 벌지않았냐?|0 +아니 한예슬 맞냐? 너무 늙었는데|0 +불편한 이질적 이견을 제시한겁니다.|0 +뭐라고 갈궜길래 비행기 뚜껑이 열렸냐|0 +보수가 또 주작을... 주작이 없으면 일베를 못와요...문재인되고 부동산 시대 저물었다고 집팔았다네ㅋㅋ 망상해서 일베오니 좋겠네~ㅋㅋㅋ|1 +월2천이면 상위 몇%냐??|0 +못배웠으니 저러지 쯧쯧 수준 알만하다|1 +ㅋㅋㅋㅋㅋㅋ 훨 낫네|0 + 그러니까 니가 큰 나무에는 못 쓰니까 쓸모없다면서요?|0 +여긴 진짜 신기하게 특출난 메인보컬이 없다 다들 와꾸 딸려도 메인 하니씩은 꼭 집어넣는데지효빼면 다 심각한 수준이던데|1 +ㅋㅋㅋㅋ 여자는 칭찬 해주고도 맞았네 ㅋㅋ|0 +일본이 대구하네요.|0 +ㅗㅜㅑ|0 +요즘 중국인들이 들어오긴한다지만 이건 잘 모르겠고 수요 없어지면 조만간이다|0 +비하는 맞지 근데 이정도로 난리치는거엔 공감이 안감|0 +누가 안전밸트 매지말래? 풉킥킼ㅣ|0 +제발 집행유예로 끝나지말기를.|0 +도태남|0 +나중에 병원에서 돈 다 쓰고 뒤진다|1 +길가다 짱깨어 떠드는 좆선족들 배에 칼침 놔줘라 |1 +므ㅓㄴ 씨벌 ㅋㅋㅋㅋㅋ|1 +자연의 기본 섭리를 거스르지마|0 + 이제 남성도 성인지 감수성에 대해 자각하고|0 +내가 반일 빨간약 먹은 걔기가 연대 출신 사회학 교수 수업듣고 부터였는데 ㅋㅋㅋ 성향은 좌빨이긴한데 식민사회 연구하면서 조선은 식민지 중에선 사정이 엄청 좋은 편이었고 일본이 조선에 돈 갖다 붓기만했다 한국의 민족주의 같은 정신적인 것부터 경제개발까지 일본 영향 안받은게 없다고 하드라고 하긴 팩트를 알면 어쩔 수가 없지|1 +ㄹㅇ|0 +펨코에서 일본 존나 빠는새끼들도 일본 살다보면 아 한국이 그립구나 할텐데|1 +산성비가 뭐야 그럼?|0 +넌 직책이 뭐냐|0 +황모씨 진급도하셔서 살림살이 좀 나아지셨나봐요ㅎㅎ|0 + 지금 여자친구랑 결혼할거라서 ㅇㅇ|0 +ㅅㅂ 언덕이 70도는 되보임 ㅋㅋㅋ|1 +병신새끼사표쓰고 나와서 T.O 하나 생기면 그자리는 또 시체팔이 민주팔이 씹새끼들한테 줄서기 바쁜 무능한 개새끼가 앉을게 뻔한데문가새끼 좋은 일 해놓고 뭐 잘했다고..어휴..검사라는 새끼가 생각이 저래 짧아갖고..ㅉㅉㅉㅉ|1 +ㅇㅇ레알로 김치유행함ㅋㅋ|0 +그럼 본인이 리필을 좀 하지...|0 +존나 띨띨해보이노|1 +ㄹㅇㅋㅋㅋㅋ|0 +초1,초2때라 기억은 잘 안나는데 문방구에 빨간마스크 시리즈 수첩으로 파란마스크 노란마스크 짭퉁 만들어내기도했음ㅋㅋㅋㅋㅋㅋㅋ|0 +?? 저건 여자들의 평소 행동 아니야?저런것 밖에 본 적 없어서 몰랐네..|0 +누구임?|0 +전설의 사건이지 ㅋㅋ|0 + 또 공장일이 ㅎㅌㅊ라는 것 역시 논점이지. 공장일이 ㅎㅌㅊ라는 게 논점이 아니었으면 '비하, 비난'이란 말 역시 나오지 않았을 거니까.|0 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ피로감 지랄 씨발 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ다른 시대에 살다왔냐강점기 시절에 살다온 산송장이면 ㅇㅈ|1 +와... 이런 오지는 백마들이랑 섹스러운 사진 찍다보면 떡도치겠지?|1 +호남형관상이네|0 +얘쁘네 ㅎ|0 + https://www.youtube.com/watch?v=4tnddmWdFo0|0 +여자가 개또라이년맞으니까 저런년 걍 방생하고 죽이면된다.절대 남자가 백번 잘했다.|1 +하.. 제발 저때로 돌아갔으면|0 +아무것도 아닌것처럼 씨부리면서명의 이전에 목숨거는게 간악해보이는데? ㅋ그게 아무것도 아니면 지년 애비 집 지분 일부 남자에게 명의 이전 좀'시원하게' 해주지 그러노 교활한 한국년들|1 +홍어.|0 +한녀 : 어쩌라구요? 아~ 감주 오늘 여탕이네 시발|1 +맞제? ㅋ ㅋ ㅋ|0 +반일선동 ㅁㅈㅎ|0 + 괜히 돈 많이 받는거 아니다 ....나 취성 패 할때 용접 6 년 하고온 나보다 2 살 형 있는데뭔가 눈도 침침하고 계속 건강 악화된다고 그러더라 그 형은 마지막 1년동안 공수 28 정도 받았다는데 ...건강챙겨서 돈많이 벌고 다른거 안전한거 슬슬 찾아봐라|0 +나도 75인치 사려고...요새 플스하는데 화면 좆만해보여서|1 +몇몇 쓰래기같은 댓글들은 더이상 상종안할랍니다.|1 + 이영상 보셈 https://youtu.be/Kph0zraXQ14|0 +요로콘데-!|0 +9임|0 +어케 따라하셨대요 ㄷㄷ|0 +내가 봤을땐 정말 진심으로 신이란건 없는것같다...진심....ㅠㅠ|0 +저게 틀린말이 아니면 요기요에 손님한테 빙신새끼, 네 다른데서 시켜쳐먹으세요~ 이런식으로 사장이 써놓은 댓글도 틀린말이 아니지. 개인 사업자로서 욕먹을 말임. 괜히 대기업에서 손님한테 아가리 안터는줄아냐??|1 +역시 배우신 분이라 다름.|0 +근데 브라는 왜 더 안까는거냐?|1 +박나애 송가인 화사|0 +조샌징이 조샌징 하네근거를 보여라 근거|1 +개독들은 나보다더한 지옥갈꺼니까|1 +좆메카지.개극혐ㅋㅋㅋ|1 +상폐 ㅁㅈㅎ|0 +그것도 부끄러운 역사가 아닌가 마지막으로 질문하고 싶다|0 +ㅋㅋ미친놈 ㅋㅋㅋㅋㅋㅋ|1 +노가다 하루벌어 10만원 버는애들한테 5천원을 빼앗아버림 저 썅년은 송장될때까지 사면하면안됨|1 +공무원 = 세금 착취하는 현대판 양반 |0 +공시생 샘통이내 절대합의하지마라|0 +나베,, 좀 더 버텨 개최한다고.. 우리는 굿이나 보고 떡이나 칠께??ㅋㅋ|1 + 하다보니 안되서 아시발 졋다 그래 하고 걍 쓰러진거가틈|1 +gov만 들어가면 무조건 다 공식싸이튼줄 착각하는 미드보고 미국배운 조선놈ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ. 어여가서 단어외워야지..??? ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +팔레비 왕조 당시의 기득권층은 저리 주장할수잇는거지.마치 전라도인들이 박정희,전두환 개새끼다/이명박~박근혜때보다 지금이 더 낫다고 하는거처럼|0 +ㅋㅋㅋ 누구나 할수 있는 생각이지만입밖으로 꺼내느냐 암묵적이냐가 문제인데재는 자기도 모르게 순간 풀려 버렸네 ㅋㅋ뒤늦게 아차선천적 본질을 숨기고 남앞에서 항상 척을해야 하는 경우라면항상 본심이 나오지 않도록 입조심을 철저히 해야함이미지 조작이 한번깨지면 그냥 김구라 노선으로 갈아 타든가|0 +개돼지 대구민들 ㅜㅜ|1 +오‥ 맞는 말이네요ㆍ|0 +지금 시발 나라 굴러가는 방향이 저쪽인데 어쩌겠냐? ㅋㅋㅋ복지에 돈 퍼줄테니 알아서 타먹으라고 돈을 드리붓는데뭐 타먹어야지 ㅋㅋㅋ내가 안먹으면 남이 먹을건데 어차피 ㅋㅋㅋ|1 +너무 나가는 막말에 기가 막혀 말문이 막혀있을 때 |0 +저저저 저 에라이시키 보소.|0 +중요하지만 현상유지로 만족하고 쓰레기 청소부터|1 +국뽕코리아|0 +다른 일게이꺼 복사 붙여넣기 한거보소 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅁㅈㅎ|1 +암페타민드립 존나 오랜만이네|1 +이런 분이 꼭 당선되셔서 공존과 공생의 뜻을 펼쳐주면 좋겠습니다.|0 + 일게이들은 빅픽처를 못그리는 븅신들임|1 + 맥주 먹으면 화당실자주가고싶어지니 나같은 술쟁인 통로가 편한거|0 +그레잇 ~~~~~~~~~~~~~~~~~~~~~~~~~~ 한국에도 카스트 제도가 있다 ㅋㅋㅋㅋㅋㅋㅋㅋ|0 +삼성전자시가총액 20분에 1수준임... 그냥 대기업 계열사 연매출 수준도 안됨... 기레기들 조단위나오니 큰일이라고 떠들어대네 국가부채가 1700조원에 대비해서 보면 12조는 그냥 진짜 껌값입니다|0 +뭔 개소리냐........ㅋㅋ|1 +못생긴 놈이 진지하게 기타 치는데 진짜 꼴보기 싫음|1 +1500000000 ㄷㄷㄷㅁㅈㅎ|0 +고중퇴에 전역하자마자 일식 배워서서른 중반에 식당 두개 운영하고 월 800정도 번다학력은 돈 버는거에 있어서 전혀 상관읍다 이기나보다 중고때 공부 잘하던 친구들그냥 대기업 다니면서 돈얘기나오면 나 부러워하고 그른다비교대상은 아니지만 돈이 최고인 세상에서 뭐 그르타|0 +저정도면 친딸 아니라고 봐야|0 +각 지역의 고등검찰청이 외부인으로 10명 임명한다는데 사실상 뻔한거 아니겠노.|0 +안전은 개뿔. 돈 못 갚으면 집 날라감|0 +박스할매 땡잡았노 싱글벙글|0 +저 새끼 파보면|1 +1사 겐뻬이 만세!|0 +80 90년대 선생들 단골멘트인데 공부못하면 공장이나가ㅋㅋ 영상에서 저지랄했으니|1 +지랄병하고 앉아 있넹...|1 +역시 렉서스 배운자의차|0 +나 일베 한번눌렀는데 왜 2베뜸?|0 +좀 못해도 같은 기술 가진 젊은 사람쓰지|0 +노잼노스트레스더블에이석섹스 댓글문화도 어느순간 사라짐ㅋㅋ|0 +노가다 안한다고 친절하게 설명해줘도 끝까지 우기는거 봐라 ㅋㅋㅋ막무가내로 고향 바꿔버리는 틀딱새끼급 ㅋㅋㅋ고바노|1 +나머진 사각턱 유전자도 없고 쌍커플은 태어날때부터 있었음|0 +다음주부터는 매일로 시작하는 신문사나 데일리로 끝나는 신문사 받아쓰기에 동참하는지 지켜봐야겠네요.|0 +팰리 미국에서 생산 안한다|0 +돈가방이랑 여드름브레이크 꼬리잡기 급 땡기네|0 +캡틴윈터스 숨겨서뭐하냐고? 양성이 나오면 안되니까 정부에대한 비난이 솟구치고 노약자뿐만이 아니라 건강한 십대에게도 치명적인 바이러스애대한 국민불안이 증대되니까|0 +알곤용접이 갑이다이기|0 +어이 나까무라! 빨리 일해!|0 +이새끼가 남들 수준 운운하니까 존나웃기네|1 +키보드워리어는 아니구나. |0 +못생긴사람 면전에 못생겼다하면 잘못한거아니네? 진실이니까?|0 +코리안 바비큐라는 스타일이 있긴 한거임??? 뭐 별다른 특색을 모르겠는데|0 +이런 저런 이유들이 있지만 환경적으로도 우리나라랑 비슷한 위치에 있는 나라들 특징이 동쪽부터 개발하는거다...편서풍이 부니까....개발이란게 결국 공장짓는건데공장만 지으면 되노??전기가 대량으로 필요하니 발전소도 지어야제??원자력 지으면 되지만 현재도 문씹쌔같은 머저리 새끼가 대통령 해처먹는데 과거엔 그런게 어딨노무적권 화력이지 매연 오지고 미세먼지 오지는데 편서풍 부는 나라서 서쪽을 개발한다???자살하겠단 소리지 |1 +이런 똥글 일베 올라오는거보니 ㅋㅋㅋ 일베수준보소|0 +'대구'|0 +장난이라하면되지 ㅂㅅ아|1 +이륙하기전에 안전벨트 메라고 쪼아서 승무원이 귀찮아서 걍 계속 차고 있는뎅|0 +똑같다 ㅋ|0 +빨갱이소굴인정|1 +사실 현지에선 어려움을 토하는 거 보다 차분함솎에 이겨내시더군요...|0 +지금 부들대는 부류=땜쟁이 or 자칭 기술직이라착각하는 노가다 가축들 ㅋㅋㅋㅋ|1 +양성 판정 받기 바란다|0 +춤 언제 추냐??|0 +근데 남자가 억지로 남자의 후장에 삽입이 가능함?그게 힘으로 제압했다 해도 괄약근 똭 힘주고 있어도 삽입이 되려나. 여자랑 다를듯 한데|1 +결혼 남자한테 너무 불리한 제도다 결혼 하지 마라...|0 +충청도 바보들이 자기 밥그릇 뺏긴거임|1 +맞는 말 해도 지랄 시발련들아|1 +제발 죠져주세요 서울 경기도권 짱개새끼들도 덤으로 날려주세용|1 +그냥 뒈져 ㅅ 벨 롬아.|1 +정상 딱 이생각듬|0 +어이 명륜친구 진사답게 행동해|0 +초창기 야스오는 개사기아니였었나? ㅈㄴ 쌧던걸로기억하는데|1 +어차피 그건 인과관계가 입증 되지도 않을듯그냥 쇼..ㅋㅋ|0 +ㅋㅋㅋㅋ아니 적당히 뇌피셜이어야지 상식까지 뇌피셜 해버리면 어떡해|1 +솔직히 일반적인 중견,대기업 다니는사람들한테 용접은 저런이미지 맞음...나야 주위에 몇명있어서 존중하지만|0 +내가 그래서 신해철 살아있을때 악플 달았었음|0 +따일 화산 따였뿟노이기|1 +오늘아침에도 뉴스댓글보니까 매크로 오지게 돌아가더만 팀이몇개나있을지모른다|1 +구청은 그나마 이용객이 많아 바쁘기라도 하지. 동네 주민센터가봐라. 존나 한가함.여간한 서류발급도 전부 무인기로 가능해서 일행직 공무원 자체가 불필요함. 존나 행정력 낭비임.이런 새끼들 싹 다 구청-시청 사회복지쪽으로 돌려야지. 정작 시청에선 업무과중 호소하다 자살하는 공무원도 있는 판에 복불복 존나 심하노이기|1 +집팔은 ㅋㅋㅋㅋㅋ 존나 신기한 말이네보통 집판 이나 집판매한 이라고 하지않냐?|1 +바보대구..ㅋㅋㅋ|0 +니가좋아한다고 잘못을 인정안하는것은|0 +신차가 150 뜨더라 첫차 30초|0 +공군썰 흔치않아서 ㅇㅂ군생활 2년 1개월동안 가장 좆같았던 때가 말년에 BAT파견 나가서였음근데 규모가 작다는거 보니 전대급인가?|0 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 팩폭 존나 쳐맞고 망신 당하더니 조용~하노. 착해졌네. 엌ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +씹엠생오타쿠일뽕좆병신들이 지들인생좆망한걸 울분토하는 진짜감정쓰레기통으로 전락해버렸음|1 +그 영화 이후로 마루타라는 신화가 생김|0 +누구긴 누구야 너지 씨발아 ㅁㅈㅎ|1 +ㄹㅇㅋㅋ 뭔가 그쪽업계 관련자래서 전문적인 대목이 나오길 기대했는데|0 +누구나 살짝씩 휘어있다 그래서 좌지우지라는 말이 거기서 나온거임|0 +독립운동이라는 말씀에 절로고개가 끄덕여집니다.|0 +임진때 식인기록이 있다는데 625때도 있었음?|0 +같은 지역을 만나니 반갑네요 ㅋㅋㅋㅋㅋㅋㅋ|0 +고속도로에서 목숨 내놓고 운전하는 병신들 존나 많아보인다 ㄹㅇ좃도 안나가는 차로 미친듯이 삐대고 ㅈㄹ 하는새끼들부터추월차선도 아닌데 뒤에 바짝붙어서 따라오는 병신까지블박차주도 존나 병신같네앞차 브레이크 들어오는데 브레이크 안 밟고 멀뚱멀뚱 쳐 가고 앉았다 시야확보도 안되는데 씨벌 ㅋㅋ개꼬시노|1 +ㅋㅋㅋㅋㅋ 근대 넌 짤닮음 ㅅㄱ ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +ㅠㅠ 코로나 끝나면 뵙고 와야겠네요...|0 +네 다음 대리충|1 +신빙성있노|0 +그래도 성실히 살아온 게이같은데 남들 한다고 결혼 한다는 생각을 했다는게 조금 의아하노 .. 뭐 나보다 인생선배에게 뭘 얘기하겠냐만 .. |1 +개 젖같은 상황이 발생하는구나~~~|1 + ㄹㅇ 첨에 갔을때 여기한국맞나 싶더라|0 +남자 선생이 7등급이면 설거지나하고 돈이나 버세요 했다간|0 +집에있는 25만원짜리 가검 휘두르다창문깸|0 +프롬프트 읽으면서 좌우로 사람들 둘러보며 말하는거 보면연기력 하나는 탁월 한것같다|0 +당연한 얘긴 쟤한테 하세영|0 +재들도 조선에게 보고 배운거여 전에은 저런거 없었지|0 +ㅇㅈ 한국인이지만, 한국놈들 잘해주면 권리로 알거나 이용해먹을려고 한다.일 하는거 보면 차라리 탈북민이 의리도 있고 강직함.중세시대 스웨덴 용병같다. 고용주가 잘해주면 고마워 할 줄 알고, 자기 선을 잘 지켜준다. 이상있거나 뭐가 지시하지 않은 일 할려면 바로 해야할지 말아야할지 묻고 실행한다.그래서 더 호의를 주고 싶음.조선족은 그냥 개새끼고|1 +윤서인?|0 +썩열이 썩어도 너무 썩어서|0 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 누구냐 ?|0 +저 여동생이면 ㅇㅈ한다|0 +실제로 성착취물 공유방을 텔레그램을 만들었다.|0 +이제 다 만들었을려나? 수천만원짜리 개인화기 ㅋㅋㅋㅋㅋㅋㅋ|0 + 꼬라지하고는|1 +골반,엉덩이 보형물은 시발 존나 이상할듯 기본적으로 골반은 뼈인데 |1 +이래서 수시충들 극혐하는거다씹쌔끼들|1 +전라도 더러운 습성이 저 만화에 다 들어가 있음 남탓에 당한 놈이 빙신이지라에 악랄한 지 놈이 피해자란 피해망상에 약자에 대한 한없는 잔인함 ~ 2% 존경스런 탈라도 제외, 인간의 탈을 쓴 악마새끼들|1 + 우파는 무능해서 당한거 맞아|1 +돈은 관리 더잘하는 애들이 같는게 맞지 병신아|1 +중국뿐 ? 요즘은 똥남아도 많이 간다|0 +나도 턱수염 레이저제모 3회에 30인가 했는데1회하고서 한동안 안가다가 보니까병원없어짐ㅋㅋㅋㅋㅋㄱㅋㄱㄱ|0 +ㄴㅈㅎ|0 +햐~~~ 시발 욕나오네|1 +정보처리기사 들고있는데여기 ㅈㄴ 양심없이 끼어들수가 없는 개씹쓰레기 기사자격증 ㅇㅈ?|1 +개최국은 자동진출.|1 +이제 4월이면 법을 제정하는 국회의원 선거가 있습니다. |0 +이탈리아가 왜 저지경인지 단박에 알수가 있네...ㅋㅋㅋ|0 +동물들하고 지내다보니 동물인지 사람인지 가끔 헷갈립니다.|0 +그래서 지금 돈 벌고 있고 이번년도에 원서 넣어볼까 싶어서|0 +대구 짐승들은 광주한테 열등감 있나 ㅎ |1 +지금 정부아니라 전이나전전정부같았음|0 +K1이 너무 작다보니까 업그레이드가 도저히 안되서 만든데 K2잖아.|0 + 성공한 여성 CEO들 보면 남성상이 강함|0 +노이건 이냐?|0 +이게 웃긴게 '일본한테 하는것처럼 중국한테는 왜 못하나'라는 반응은 이해가 가는데 '중국한테 기는것처럼 일본에는 왜 안 기나'라는 분들이 펨코에 ㄹㅇ 엄청많아|0 +아니 쟤네 맨날 인스타 하고 맨날 싸돌아댕기는데 공부할 시간이 있나 싶어서 ㅋㅋㅋ|1 +누군가는 똥을 쳐야하고 내가 치기싫으니까 똥치는 사람 배려해주자 하면 될걸 존나 어렵게 써놓았노 전국의 분뇨관련 종사자들이 들고 일어나면 온국토가 똥바다 되는거지|1 +일상이라 침착한거 보소|0 +지방= 언어만 같은 한국말쓰는 동남아인지방과 동남아의 차이 = 방문할때 여권 필요하냐 아니냐의 차이만 있을뿐수도권으로 몰려드는 지방충이랑, 한국으로 몰려오는 조선족/동남아의 차이가 뭐냐 솔직히? 지방충들도 솔직히 똥남아,짱개랑 동급으로 취급해야함그리고 균형발전을 할게 아니라 오히려 지금보다 더 수도권을 키우고 키우고 더 키워야함글로벌 경쟁시대에 뉴욕,도쿄,런던 같은 국제도시들이나 중국 대도시들에 경쟁하려면.|1 +살아가는데 아무런 지장도 없던데? 지랄하지말고 ㅁㅈㅎ나 쳐먹어라. 인생은 어차피 자기랑 맞는 사람과 살아가면 되는거다. |1 +이걸밝히는사람진짜대단..|0 +아ㅋㅌㅋㅋㅋ|0 +니 보고 그러는게 아니다...여기 보면 개 싸가지 새끼들 많지...노인을 공경하는것과 예의 없는 나이 많은 사람을 상대하는것은 분명 다른거지...|1 +정몽준은 통계에서 빼고함|0 +가장 좆만한 새끼가 존나게 깝치는 건 세계공통이냐ㅋㅋㅋ|1 +허리띠 안 했다고 패고,|0 +부캐임 ㅋㅋ|0 +피해를 왜안줘 씹쌔끼야|1 +팸코충 ㅁㅈㅎ!!!!!|0 +닭|0 +다음글예상 : 위기상황에 통화스와프에만 메달리면 안된다|0 +비슈누?|0 +깨져도 모르는 정도를 넘어섰다|0 +헐..|0 +농어촌전형 자퇴생 답네.|0 +소파 포기해야하고|0 +그렇더라|0 +ㅋㅋㅋㅋㅋㅋ|0 +느그애미 수원에서 외노자 보지대주다가|1 +복귀해야되는데 집이없어서 걍 자살함|1 +그대는 흐엉밖에 없다|0 +얼마안가 자살당하시는거 아니냐|0 +군시절 1호차 2호차들|0 +본점인데 ㄷㄷㄷㄷ|0 +자위질하는거 보면 섬숭이랑 비슷한 수준이네.|1 +그럼 다른 대기업들은 학벌로 일단 자르는건가? 건동홍급 어문계열은 대기업 서류 광탈이냐? 아님 기회는 주냐?|0 +터질것 같아 조마조마 봤는데 |0 +고향이 어디노|0 +글쓴님 본인부터 아내분에게|0 +어깨 으쓱하면서 입씰룩이는거 존나 귀엽네 ㅋ|1 +그럼 땅에 뭍으라고??? 뭐 어떻게 하라고저런덴 쓰레기 수거를 안해 병신아 |1 +맨날 하는 뻔한 얘기 그냥 방송에서 하는거임|0 +깡패색히.|1 +너가 병신이네 ㅋㅋ|1 +아 너도 창조론 믿음?|0 +너같은놈은 뽑지도 않을듯..|1 +명작이지 작전 |0 + 불합리한 공공의료정책은 니 뜻에 동조하는 의사들을 모아서 캠페인을 벌이든가 시민을 설득하고|0 +부랄 달랑 시발년들아 |1 +안구워짐?|0 +무섭다시리즈 어중간해서 별로안무서웠는데다른만화책에서 사람 발바닥에 구멍뚫어서 피뽑아먹는귀신이랑 머리만 떠다니는 귀신편이 무서워서 잠못잠|0 +응 정신승리 백날해봐야 용접공들은 신분상승안돼 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 돈많이버는게 장땡이라고? 백날 돈 처벌어봐라 , 허드렛 좆가다 몸병신되서 만날 수 있는 보지의 카테고리가 어디까지가 한계인지 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +내친구 한명 기계공학과인데 공조냉덩이랑 일반기계 쌍기사 땃음|0 +사실 법이 그런데 어쩌라고|0 +씨발 내가 너 좋와하면 안되냐! 내가 너 좋와하면 안되냐! 내가 너 좋와할수도 있자나 이런 씨발 세상 좃같은 것들이 왜 나한테 지랄들이야 그래 안그래!!|1 +역시 베트남.. 개 싼마이 국가..|0 +아니 키좀 크고 착하게 생김..애새끼가 순진하게 행동했는데 오히려 거기서 여자들 무장해제... 들리는 소문엔 대물이라고 하더라|1 +애기 보호하는 고양이 영상 봤다|0 +아니 국내랑 꼭 똑같이 팔아야되는 이유가 있나요??|0 +검정 롱패딩보단 보기좋네|0 +한국 욕하는건 자연스러운 행위임|0 +남의 나라일에 왜?|0 +신뢰를 안주니깐 그러지 돌대가리년아;|1 +생각을 해봐라 트와이스인데 동네 핫바지 음향업체가 들어갔겠냐 ㅋㅋ|0 +아무리 바짝 땡긴다해도 허리인데...|0 +7등급이하면 할수있는일자체가 거의없고 일용직 노가다 알바 이런거밖에없는데펙트로만 이야기하니까 딱히 반박할수있는게없음.|0 +정신차려 병신년아 너따위는 발가락의 때한테도 못 비빌정도로 예쁜애들이 득실거리는곳이 러시아니까..|1 +데리고 와서 혹시나 성공 못해도 EPL 홈그로운에 23살이라 타 클럽에 판매만 해도 바이백 금액 이상으로 돈 회수할 수 있음산초 영입 떠나서 무조건 데려와야 함 사실상 다음 시즌에 윌리안, 페드로가 아웃이면 풀리식, 오도이 밖에 없음|0 +ㅋㅋㅋ게이커여어|1 +전부 대본이잖아 ㅋㅋㅋ 이거 유병재가 보고 재밌다싶어서 오케이했을 듯|0 +중세 잽랜드 원숭이새끼들.발가벗고 돌아다니던 것들을.우리 선조 백제인들 가야인 선조들이.기저귀부터 채워줘! 밥멕여줘 옷만들어줘 농사가르쳐줘.일본왕되서 통치해줘.이제는 발달된 선진 인권 민주주의도 참교육으로.전파해줄 타이밍인가?.하여간 귀찮은 원숭이들 ㅋㅋㅋㅋ|1 + 주님을 보라구 |0 +개독 지옥|1 +아 그래 나도 차라리 투표장 안가려고|0 +갑자기 지디게이 부러워지노|0 +교도서 ㅁㅈㅎ|0 +ㅋㅋㅋ 더듬어 만져가ㅈ해냈네|0 +저새끼 일베 ㅎㅓㅅ한듯 ㅋㅋㅋㅋㅋㅋㅋ|1 +나 마린시티에서 오또맘은 실제로 봤는데본인 인스타에 올라오는사진이랑 비슷함|0 +용접공, 배관공은 어느정도 ㅇㅈ해주자! 이기!|0 +저런거 보면 동물을 먹지 말자는 사람들의 논리도 조금은 이해가 간다|0 +설날이여?|0 +ㅅㅂㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +내가 주위사람에게 들었던 편모딸 썰 2가지1 딸이 엄마남친에게 강간당했는데 엄마가 남친과 헤어지기 싫어서 딸보고 참으라 함2 편모딸이랑 사귀어서 엄마집에 놀러갔는데 엄마가 남자를 유혹함이혼한 엄마와 딸만 사는집안이 막장의 극강 조합이다 이기야|0 +블박 = 본인|0 +님 말대로면 전국민 다들 부자되겠네 ㅉ ㅉ|1 +요시 렉서슨 잡아버리자 이기! 하는게 현실인데 ㅋㅋㅋ|0 +이유없이 니생각대로 나보다 논리적으로 생각을 잘한다고 말한다는거 자체가 무식한거고 못배웠거나 지능이 딸리거나 주의력이 부족하다는거임 인간은 다른사람보다 분석력 주의력같은 지적능력이 좋아야 성공한 인생을 사는거 맞지 않냐?|1 +진짜 일뽕글 올라올때 마다 졸라게 웃기네현금이 많아서 살기 좋다니 ㅋㅋㅋㅋㅋ이새끼들 진짜 일본에서 살다온 놈들이냐 ㅋㅋㅋ일본 은행 한번이라도 이용해 봤을까 ㅋㅋㅋ|1 +니 글 봤더니 칼로 사람 배떄지나 목에 쑤셔넣고 막 비틀고 휘젓는 상상했음..|1 +짱퀴벌레냐?|1 +충전속도 짧아지면 많은양을 한거번에 넣어야 하는데 댁이사는 아파트단지 변압기 100배가 넘는 용량 으로 증설해야된다고요 . 집마다 2-3억씩 관리비 한큐에 가능할꺼임|0 + 대다수가 못 살고 못 배운 넘들이라 평판이 안 좋음|0 +개씹뜬금없네 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +텔레그램 n번방 검색해보니 안나오는거 같던데 폭파되었노 |0 +강|0 +이야 현명하네 받을수 있는거 받아야지 씨발|1 +아놔 이새기 여기 숨어잇엇네 ㅋㅋ 야 시발새꺄 내가 오늘 이만원 갖고오랫지 뒤질래?|1 + '한일군사정보보호협정'도 미국이 강권해서 체결한 것이고,|0 + 그 소방관새끼 조국 실드치다 공정한 척 말 씨부리다 나이 30에 그게 할말이냐??????|1 +어쩌라구 병신아;;;|1 +그래서 안 봄|0 +1123은 1.75인데 이 점수대도 백분위 잘 받으면 중대수학 가는거고|0 +신고함|0 +잘 몰라서 그러는데 여자들한테 무슨약점을 잡았길래 저지랄을 할수가있는거야?|1 +평균같은데|0 +올림픽 취소되면.. 열심히 운동만했던 선수및 그가족들의 꿈도 날아가네요ㅜㅜ 아픈현실을 어떻게 받아드릴지~~ 정부차원에서 뭔가 챙겨야할듯ㅠㅠ|0 +공부 못하면 그딴 하찮은 일 밖에 못하니까 노력해라..라는 뜻으로 얘기한 거겠지|0 + 넌 좋은곳만 다니니 알 턱이 있나~|0 +거짓과기만의 인권팔이가 민중에 의해 끌려내려오게 된다는 뜻인듯요즘 역전앞에 태극기부대들이 문재앙틴핵 서명받던데 이미 일이 시작되고있는지도 모른다|0 +증거 좀 보여주라|0 +아직 멀었습니다~|0 +아이폰11 s11 겹쳐서 비교 ㅈㄴ 당하니까 삼성이 출시연도에 맞게 바꿈 2020=s20 2021=s21|1 +저번에 국밥집갔는데맛도 씨발 별로면서 존나 씨발 싸가지없길래 사장 보는데서 국밥에 침뱉고 존나 씨발맛없네 하고 나가서 옆집감 ㅋㅋㅋㅋㅋㅋㅋ|1 +지랄 개소리 쳐하고 자빠졌네 보지년아|1 +비하하는 놈들 비하하는 건 개병신 중에서도 상개병신 아닌가?|1 +뭐랄까 따뜻한데..?|0 +여기서힘들게 살지말고 일본으로 후쿠시마로 이민좀가라|0 +유럽애들 상상이상으로 멍청하다고 말만 들었지 이정도일줄은 몰랐다 ㅋㅋㅋ|1 +수학강사든 용접공이든 개좆도 아닌 일들하면서누가 낫네 지랄들이노ㅋㅋㅋㅋ사회에서 밑바닥 직업층에 속하는데 누가 낫냐고 비교질하는게아니꼬울따름임ㅋㅋㅋ |1 + 경북신문사설 전원책 조중동 등등 나처럼 근거들을 말해야지|0 +여자 하나 먹어봣으면 하고 울던 아재 짤 아들인줄ㅋㅋㅋ|1 +옛날에 의사 인증한 일게이가이국종 나쁜사람은 아닌데 환자 배 갈라놓고 가족불러서 설명한다고 관종기질있다고 그랬는데 ㅋㅋ|1 + 꼴랑 600만원에 무슨 취업이민 보장한다고 사기를 치노 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +저건 사실도 아니잖아죽이는 기술 ㅋㅋㅋㅋㅋㅋ|0 +진짜 쟤가 인터넷 방송 대통령이 틀린말이 아닌거같다 지금 10대중에서 기모띠 안쓰는 얘들이 없음 앞에 무조건 앙 무슨무슨 띠 다 붙이고 |0 +ㅇㅇ 근육때문에 장이 강화됌|0 +너 수능 몇등급이야?용접등급입니다.입에 착착 감기고 좋구만.|0 +예수믿는새끼들 전부 강간범새끼들이여..|1 +날갤 일주일 정지먹어서 대피소로 호다카 갤러리 씀|0 +수조에있는고기는 언제꺼냐?|0 +마크롱이 전국민 막으면 너무 잘하는짓 박원순이 교회가지마라 하면 쇼 하나만해라|0 +머리에 밀웜을 꽂고 다니노|1 +못생긴 니가 이해해라 울지말고 얘기하고|1 +레디 주입식 간지 아니엿나|0 +없어짐|0 +아주그냥 콩깍지가 제대로 꼈구나..|0 +와 저와중에 트럭 옆으로 비켜주는거 진짜 센스 지리네;;|0 +ㅋㅋㅋㅋㅋㅋㅋ 꿈깨야될듯. 순위에도 없는 그랜져를... 그와중에 G70이 엄청 빠름요 |0 +베트콩들 왜이리 나댑니까|1 +참고로 서울산다 = 전라도|0 +쓸데없는 소리하지말고 수학 문제나 다 플어|0 +성평등...그는 평등하지 않았다|0 +은평구 달동네 진짜 무서움.. 밤에다니면 지릴듯|0 +쪽국은 아직 시작도 안한셈..아마 제대로 검사시작하면 진정한 좀비국될듯...|0 +옛날 일베하고 지금 일베하고 비교하자면지금 일베글들이 조회수는 훨씬 더 잘나오는 듯페이지 5 이상만 가봐도 기본 7~8만에좀 자극적인 제목이면 10만 가뿐히 넘음근데 옛날 만베 글들은 조회수 봐라지금보다 못함눈팅족이 많아진듯|0 +갑자기 항공방제 비행기가 나타난 느낌?|0 +대구교대|0 +시발 무려 이국종도 저딴 욕 들으면서 살아야되나진짜 인생 좆같네|1 +직업에 귀천이 없어야한다라는 의미로 쓴 게 아니란거다 |0 +곧 3000갑니다 안전밸트 꽉매세요|0 +그거 다른애 글에있다 찾아봐라~ |0 +나도 보고 왔는데|0 +니 애미도 장애인임?|1 +나도 그렇게 알고있었는대|0 +현실을 얘기한거다. 택시라는 직업을 비하한게 아니라 택시기사라는 직업의 수입에는 한계가 있으니까.|0 +아니 노예 해방시켜준 일본한테 고마운거면 니들이 일본국민이 될 기회를 박탈해버린 미국은 왜 빠는거냐고 ㅋㅋㅋ|1 +민주당 배재정35.87% 무소속 장재원 37.52%|0 +둘다 너네집보단 나은거 아니냐|0 +지랄발광|1 +박사모라 ㅈㅎ|0 +저분 젓가락질과 크게한입이 저짤을 살린겁니다 ㅎㅎ|0 +기술 필요해 해저용접 같은거는 특히 아무나 하는게 아니여 문제는 그런 특수용접공들이 아닌 잡용접공들이 자신을 고급기술자로 알기 때문이지|1 +낙태율 1위 강간무고1위 해외에서 백인너드만보면 다리벌리는 시간 세계 신기록 갱신등등|1 +현대가 닛산한테도 밀리는구나.... 자국시장은 그냥 먹고 들어가는데도 저러네 ㅉㅉ|1 +당시 진사면 간판급 프로 아니었나? 그걸 한번도 안봤다고?|0 +남친새끼가 보살이노 ㅋㅋㅋㅋㅋㅋ|1 +백인찐따라 그런게 아닐까?|1 +어쩌라는거냐 시속23km로 속도줄여도 사람튀어나오면 살인자되는데|0 +종교탄압이니 지럴하겠지..|1 +지방대면 서울대 인기과가 더 높음|0 +듣기로는 암묵적으로 대학컷이있다|0 +내릴때 교통카드 꼭 찍고|0 + 증기 캐터펄트 쓰려면 높은 확률로 핵을 연료로 써야하는데, 핵잠도 지금 못만드는 상황이잖아.|0 +사람도귀천이있지 뭔 글이 저따구냐???|1 +알겠다 이 미친놈아|1 +다음에 쳐맞을거 두려워서 칼질함|1 +게이 아는거 강사 스펙만 알고 수학공부만 하노??|1 +평생 같이 살지말라고 덕담해준건데 찐따가 졸렬한거지|1 +중소 100 삼성 엘지 160정도 ㅇㅇ|0 +네 다음 토끼|0 + ㅂㄷㅂㄷ 대지말고|1 +ㄴㄴㄴ 일진 세키들이 오혼 존나게 함 |1 +이게 팩트지. 터널에선 앞앞차가 안보이고 앞차만 보고 가는데, 앞차가 사고직전에 틀어버리면 어떻게 대비하나. 블박차 욕하는게 대깨문 스러운 논리 ㅉㅉ 물론 안전거리 유지 못한건 잘못이지만 그전에 앞차가 갑자기 브레이크 밟아서 거리가 좁아든건 있는듯.|1 +브금씨발 뭐야ㅋㅋㅋ|1 +페미세상이라 이게다 재앙이탓이다|1 +매니토바 시베리아 촌구석 같은 곳임|0 +호주 막힌지가 언제인데ㅋㅋ 타임머신타고 과거에서 돌아왔노?|0 +나도 현기,대우상용차,쌍용차 말고 1차2차업체다녀봐도 다른용접은 못봤다|0 +그냥 니가 개소리하니까 나도 개소리한번해봤다고하면되자나|1 +예정으로 흘리고 반응 본다음에 괜찮으면 영입하고 아니면 퇴출이지뭐 나다은 으로 식겁 하고 그래도 학습 효과는 생긴듯 |0 +아나운서 이쁘다|0 +데릴사위에 가깝다...|0 +간첩설이 아니라 간첩 맞어 뭐 이런애가 일베하고 있지? 분단국가에서 수십년 전부터 우리나라에 스파이가 없었겟냐? 확율적으로 그게 말이 돼? 전우주에 생명체가 지구밖에 없단 소리랑 같은거다|0 +ㄴㄴ 안정권이가 제대로 본게|0 +칠곡도 머구 구미 김천 틈바구니에 있는 곳이라 ㅋㅋㅋ|0 +그런 사람 많음?|0 +근데 받아들이는 사람들이 어떻게 받아들였느냐 그게 중요한 거지. 결국 본인이 어떤 의도를 갖고 있었냐는 타인 입장에선 모르거든. 비하를 예로 들면, 이 경우엔 명확히 비하를 뜻하는 단어를 사용한 것도 아니니까.|0 +알고 보니 진성 빨갱이였다더만|1 +딸이랑 피부랑 직빵으로 연결되있다|1 +MYUNG BAK NETWORK|0 +그딴거 필요없고 백마나 스시녀랑 결혼하면 승자되는거 모르노?|1 +채권추심이 그거냐 돈빌리고 안갚는새끼들 찾아가서 좆패고 받아내는거? 아님 신사적으로 하는거냐 ㄹㅇ궁금|0 +근데 과거 태평양 전쟁때 미국도 조선을 저정도로 생각했겠지근데 미국은 정말 지구반대편 이런나라에 170만명을 보내다니 정말ㄷㄷ 감비아가 전쟁중인데 우리나라가 저정도 보낼 수 있을라나|0 +요새 한 반에 몇명임? ㄷㄷ|0 + 즉 자기 자신이 제일 두려워하는 ‘치매’라는 현상을 타인에게 투영한 결과라고 볼 수 있음.|0 +맞다 전문직은 어렵다. 머리 좀 상식선에서 쓸 줄 알아야 한다.|0 +와 진짜 씨발 미친새끼들 널렸네|1 +ㅇㅇㅇㅜㅐㅜㅇㅇㅇ|0 +자유다한국당|0 +일베의 총본산임을 인증...|0 +공장가야할새끼들이 재수한다고 지랄 꼴깝을 떨어대니깐 자기도 툭튀어 나오는거지 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +곧 뒤질 사람 데려다가 뒤지면 병원비 낼수 있음? 연고자 없고 재산 없으면 그냥 병원에서 처리해야지|1 +컨셉이잖아ㅁㅈㅎ|0 +2.5겠지 시팔ㅋㅋㅋㅋㅋ|1 +내가 의료계에 있어서 안다. 예를 들어 설명해줄게 . 중증외상때문에 병원에 실려온 환자가 있는데 이 사람 사지마비되고 전신 피부에 염증이 생겨서 매일마다 손발 및 드레싱 갈아줘야한다 이깨 메디폼이라고 전신의 피부의 고름을 흡수해주은 재질의 것을 쓰는데 이게 70cm ×70cm 한장짜리 가격 물어보니까 20만원이더라. 이 사람 한번 전신 소독할때 이거 2개는 썼던것 같다. 근데 이 소독을 매일 하루에 한번씩 한다 . 근데 이 중증외상환자 집안도 좃같은게 가족들 지들은 보험이 되니까 이게 당연한줄 알고 드레싱 제대러 안해놨다고 지랄도 자주함. 이렇게 중증외상 당하는 사람들 왠만하면 사회 하층민이고 진짜 완전 돈 빨아먹는 기계다 . 이거 정부에서 다 보전해줄려면 진짜 제대러 각오해야되는데 어차피 소수이고 사회 하층민이다보니까 그냥 적당히 병원 니들이 책임지라고 던져놓는다. 그럼 병원은 그냥 안받으면 되지 않냐?의료법상 환자가 방문하면 거부를 못하게 의료법이 되어있어서 환자거부하명 처벌받음. 그래서 저런환자들 병원들이 서로 안받으려고 교수없다 베드없다 수술방 없다고 뺑이치게 하다가 환자 죽는일도 발생한다. 그치만 애초애 저런 중증환자데 대해서 정부가 제대로 지원해줬으면 오히여 병원들이 받으려고 하겠지. 아무튼 고생이니 적자는 병원이 감수하고면서도 대학병원들이 욕은 또 졸라게 먹는다 . 문재앙 이새가 멀쩡한 건보재정 마이너스 낸거를 이런데 써야되는것 같다는게 올바른 방향인거 같으면서도, 이런 환자들 블쌍하긴 ㅜ한데 보통 이런 대학병원오는 가족들 민도가 낮아서 기껏 치료해놬더니 고소하고 응금실에서 시비걸거 욕하고 이딴 짓도 하도 많이 봐서 그냥 듸져버럈으면 하는 생각도 들고 이중적인 감정이 든다 |1 +목줄이 너무 타이트하잖니?|0 +펙트 ㅠ|0 +쓸데없이 공무원들 일시켜서 강력한 유감의 뜻을 표한다 ㅁㅈㅎ|0 +쪽팔릴거 뭐있노 게이야ㅋㅋㅋ 전화해서 취해서 맛간것 같다 해라 이기ㅋㅋㅋ|1 +근데 그걸로 너처럼 불평하지 않는다. 돈도 많이 받는 새끼들이 나보다 더 널널하네 이런 생각 추호도 안든다고 ㅄ아.|1 + 견찰이 수사 종결해버릴 권리가 있다.|0 +이제 시간 존나빨리갈거다 |1 +이 누님 누구세요? 글에 품격이 느껴지네요~|0 +관심없는거였노ㅜ|0 +선거비용으로쓰고 남으면 좀 주께 우리가남이가|0 +니도 느검마 눈엔 아기천사였어|1 + 네가 순종하면 하나님의 선하심이 있으나|0 +루테인 실리마린 프로,프리바이오틱스 글루타민 종합비타민 비타민D 칼슘 오메가3이상 내가 먹는 영양제 목록|0 + 넌 감성이 좌좀 스타일임. 그렇게 감정적으로 휘둘릴꺼면|1 +영국 어디갔노|0 +폰팔이랑 노는 니가 ㅂㅅ|1 +모순을 극복하는것이 페미니즘이다|0 +다른나라들....소강상태 들어갈때 일본 뒷북 칠거 같은데.....커밍쑤운~|0 +이게 맞지 일게이들 운동선수는 몸상하는거 아니냐 노가다 아니냐해도 이쁜연예인들 만나고 결혼하는거보면 인식자체가 노가다랑 다른건데 이런걸 이해못함|1 +와~~ 대단한 팩트입니다...|0 +별로 저게 딸딸이 영상만큼 치욕스러운것도 아닐뿐더러 난 그런거 퍼진다고 해도 아무렇지않을 자신있음 |1 +ㅉㅉ 일베충이란 다른게 학력밖에 없노 wwwwwwwwww|1 +똑똑똑 나랑께문열어보랑께|0 +중국에서 유래된 것은 맞지 ㅋ|0 +빙신아 호흡기없이 뛰어내리면 디져|0 +ㅍㅌ는 몰라도 ㅅㅌ는 힘듬|0 +영화 만비키 가족 . 기생충 , 조커요즘 영화 화두가 빈부격차인 이유 세상 젊은이들이 빈부격차로 시달리기 때문에이글 팩트로 조지면 일본 경기가 좋아서 취직이 잘된다고 ? 아니 그냥 일할수 있는 젊은이들이 없기 때문에 ( 취직이 된다고 해도 일자리의 질적인 부분도 떨어짐 )그만큼 노인인구가 상당함 일본의 골치 덩이지 2 명당 1 명은 노인인구가 일본임 근데 무슨 일본 사토리 세대가 행복해 이건 반대로 우리한테 주는 반면교사다 왜 ? 앞으로 우리가 2030 년 2040 년이 되면 노인인구가 지금보다 상당할테고 거기에 대한 복지 비용을 생각하면 앞날이 깜깜하다 그때가 되면 우리고 일본처럼 일할수 있는 젊은이들이 줄어들테고 현 초딩들이 20 대 30 대가 되면 취직하는데 별 하자가 없으면 그냥 바로바로 취직 다만 일자리의 질적인 부분은 참고해야 한다 지금 일본인들이 과연 일자리 질이 어떤지 ... 현 일본은 디플레로 고통 받고 있고 아베의 엔저 정책으로 마치 경기가 좋아보이는것 처럼 보이지만 실상은 부채가 상당하다 ... 일본내 불만 여론을 잠식 시키기 위해서 한국이나 중국 ( 특히 한국 ) 뉴스로 매일 시간 때우는게 지금 일본임 이게 과연 예전 일본에서 보던 모습일까 ? ;;; 현실을 직시 했으면|0 +펭수 문재앙 정부랑 좌파언론에서 미는거라 비호감|1 +전형적인 애미 씹뒤진 일베충새끼네|1 +생활의 여유나 정겨움이 있는것 같네요 저렇게 어려웠던 시절에서 이렇게 발전을 할수 있었다는게 참 대단한|0 +내가 한 말이네. 자학개그|0 +20대에는 고환에서 적당히해 하면서 |0 +ㅋㅋㅋㅋ 미친, 즉당히 하라 마!|1 +선교님 아바타하시라니까 왜 그러셨어요...|0 +이게 나라다|0 +키큰 연예인은 일부러 조금 낮춤. 그래야 비율이 더 좋아보이는 착시|0 +저렇게 어그로 끌어도 구독자는 55만에서 안늘더라|1 +스파이 제안받고 살해 안당하는 법1. 처음에 스파이 제안을 받았을때 "중국을 위해서 하는일이니 돈은 필요없고 원하는대로 자유당 소속으로 출마해 주겠다"라고 말함2. 그리고 출마함3. 선거 유세 대충하고 낙선함4. "열심히 했는데 낙선했네 ㅅㄱ"라고 말함5. 중국 당국은 스파이 후보가 무능력했다고 자체적으로 평가한 뒤, 다른 스파이 후보 물색함6. 살아남은 다음에 속으로 "ㅋㅋ일부러 대충해서 낙선한건데 ㅄ들 ㅋㅋ"하면서 몰래 비웃어준다.|1 +색즉시공 공즉시색...|0 +엌ㅋㅋㅋ 부들부들 발끈 미친년 ㅋㅋ|1 +이정도 보고 딸쳤음 자지가 약발 없이 서겠노?좆이 주인 잘못 만나 좆대가리 거무튀튀가 될정도로 뽑아낸 모양이노좆대가리야 다음 생엔 주인 잘 만나라|1 + 너 지금 그말 자신할 수 있어? 돈 뿌리면 된다고? 난 참고로 의사들에게 무료희생하면 다됨 이라는 식으로 지껄인적 없어.|0 +동의 완료|0 +내친구네가 빌딩 30개 정도 가지고 있는데 그 친구가 정말 재벌급들에 비하면 자긴 세발의 피라고 하더라|0 +아 예 참 대단한 복지네요|0 +와 시발 진짜 최고속도로 쳐버리고싶다ㅋㅋㅋ|1 +소송|0 +시동 중국집 사장님도.....|0 +훼손될 명예가 있어야 명예훼손이 성립되지 않는가요?|0 +뭐 좆도 없을꺼같은데 나대노|1 +이명박 지지자입니다|0 +홍어유입 심각함.전라도 척살 시급. |1 +이거 딱 보수자나? 보수 대갈통에 문재인들어있는거 세상사람 다 알음ㅋ|1 +그렇게 단정지어 말하는 근거가 대체 뭐임?그정도로 확실한 상관관계가 있다고 입증하기 존나 어려워보이는데근거가 되는 논문이나 연구결과 있으면 알려주셈|1 +윤썩려리 기소하지 말라고 하는거 아닐까??|0 +1인당 gdp는 낮을수록 빨리올라가는게 맞음.|0 +노벨평화상은 따놓은 당상|0 +네 다음 반일국뽕 폭도|1 +건설교통부는 뭐냐 ㅋㅋㅋㅋㅋㅋㅋ국토교통부지|0 +어휴 송장아 주책은...|0 + 사회주의를 해서 잘 살게 된 거야 아니면 잘 살아서 사회주의적 요소를 도입한 거야? ㅋㅋㅋ|0 + 내가 별자리 점치는 타로카드 점쟁이 같은 미신 추종자도 아니고|0 +옆에 타서 훈수 둘줄만아니|0 +ㄴㄴㄴ 박정희의 의료보험은 사기 였어|0 +여기서 논리 모순이 또 생기네 ㅡㅡ카타르 사우디도 국유화 오지는 나라고, 니가 생각하는 자본주의 시장경제 룰을 개판으로 어기는 나라이며 복지국가인데, 니 논리면 베네수엘라 꼴 나야지?이렇게 압도적이고 안정적인 국부를 가진 나라들을 북한,중국,베네수엘라에 빗대는게 얼마나 이치에 맞지 않는 건지 알겠냐??|0 +늘 한국만을 위해줌 ㅋㅋㅋ|0 +용접이고 나발이고 대한민국 쓸데없는 학구열 사교육 때문에 사회적비용 낭비가 심한데 저런 발언해서 사교육 부추기는게 좆같긴하지 |1 +알아서 자정능력이잇었는데|0 +병신안동이야말로 감주(단감 술주 단술)와 식혜가 엄격하게 구분된 곳인데...서울촌놈이 말하는 식혜가안동에서는 감주다.|1 +지인도 탈탈털렸던데|0 +결론이 씹새끼네 . 지금도 어린여자랑 살고싶다야뭐야에라이 3억주고 낳은새끼 트럭에 밟혀뒈져라|1 +동물구조 부르거나 유기견 보호센터가면 그냥 안락사다차라리 산에서 노숙견으로 사는게 나음|0 +ㅆㅇㅈ ㅇㅂ|0 +말이 씨가 된다 더니 ㄷㄷ|0 +7등급 이하는 부사관이나 해라 했으면 한반도 뒤집어 질 정도로 한남들 자들자들 했을 건데 아깝|0 +지랄염병하네..|1 +나경원이 얼굴 보지 말자 이제!!|0 +푸파 그레이는 이미 작년 11월부터 클론이었음ㅋㅋ|0 +왜?|0 +내맘대로 계속 깔껀데 니미씨발련아?|1 +조폭새끼들 싸움 못한다고 착각하는 애들이 병신같음 저새끼들 태반이 유도 씨름 복싱 선출임|1 +확!씨발년아|1 +내년엔 진도8 지진도 한번 올 때가 된것 같은데.|0 +꼴값떠네|1 +없으면 보급형 스마트폰 쓰면 되는데 굳이 최신폰 최상위 버전인 5g로 갈라고 하노 ㅠㅠㅠ|0 +지디게이 사랑해 일베로 돌아와줘|1 +원래 보험이 다단계식 지인팔이업종임 ㅇㅇ |0 +따뜻한 물로 샤워하고 기분풀어|0 +한국이 하면 빨갱시짓?|1 +얘도 시집은 잘가겠다|0 +ㅇㅇ개씨발창년아 안들어ㅋㅋㅋㅋ|1 +박근혜는 단죄해야한다고 직접 광장까지 나가 앉아있던 초냉정함은 어디가고 조국은 왜 따뜻하게 품어주고만 싶은걸까|0 +이 새끼들은 언제 돌변할지 모름.|1 +신천지 교도관이 밥퍼주겠네ㅡ|0 +라디오 청취율 순위1위 김어준의 뉴스공장2위 김용민 뉴스쇼3위 김현정 뉴스토크4위 컬투쇼라디오 청취율 1,2,3위가 전부 정치/시사 관련임. 즉, 더이상은 보수우파가 왜곡되고 저능하고 편협한 시각으로만 국민 세뇌를 못시킨다는 의미. 조선일보 한국경제등의 보수언론이 왜곡해서 보도하면 김어준, 김용민이 바로 팩트체크해서 다른면(other side)역시 보여줌.심지어 김어준 청취율은 왠만한 드라마 1위 씹어먹는 18% 수준.심지어 10년간 라디오 청취율 1위였던 컬투쇼는 4위로 밀려남 ㅋㅋㅋㅋ보수우파들이 얼마나 저능하게 정치와 미디어, 언론을 억압했는지 보여주는거지|1 +응아니야|1 +방사능 먹은 코로나 바이러스가 정확하죠|0 +나두 그생각 했음 |0 + 암튼 두고봐야 알듯|0 +우리뽑아준 다음날인 16일, 선물처럼 짠~ 하고 주는거네 ㅋㅋㅋ|0 +야~!기분좋다~!|0 +옆에 차가 살짝 안비켜줬었다면 휴.. 생각도 하기싫네요.|0 +동대문이랑 송파는 왜 없냐?|0 +저런 눈먼돈 아낀다고 다른 눈먼돈 안나갈까?|0 +미아 멜라노 몸매 미쳤네 ㄷㄷ;|0 +종이접기?|0 +한국인 이수진 판사 옆에 있으니까 진짜 더 일본사람처럼 보이네요... 주어는 없습니다|0 +근데 군의관이 수술할 짬은 아니지않냐?환자들 다 뒤질듯....|1 +젊은 시절 누드 봤다이기야 딱좋다이기|1 +지랄을한다|1 +그럼 전화로 시켜 병신아 |1 +앞에 차 있구만 들러붙어서 뭐 어쩌라고 ㅋㅋ|1 +나도 코갤시절에 왔는데 넌 왜 3렙이냐|0 +일단 캡쳐좀 해서 신고좀 해야쓰겄네. 보배 신고 말고 정식 신고다. 기다려라.|0 +목줄은 풀어주었니?|0 +미안 광고는 skip 하지 요즘 광고를 봐|0 +근데 용접이 돈 잘버는게현장에서 좆빠지게 구르며 생명 깎아가며 돈쓸간도 없이 버는거라서나중에 병원비로 다쓰거나마누라만 신남노가다 특성상 지방이나 조선소로 가서 기러기(?)아빠 되는데그냥 돈만 벌어다주는 기계된다결혼하지말고 지방 여자들 사먹는걸로 만족하는것도...|1 +팩트체크) 어딘가 모자라고 어딘가 열등감이 있고 결함 있는 사람들만 하위 직종을 무시하는 발언을 함 금수저에 고학력 인성 교육 잘 된 무결점인 사람들은 저런 얘기 자체를 안꺼냄 |0 +진짜 대한민국이 아닌 다른 세계를 보는것 같음.|0 +아가리좀 털어줬다|1 +강동원 실제로 못봣으면 그런말하지마라 박보검읔 상대가 안된다|0 +삼성같은데 존버하고 버티면 이기는거 아닌가?? 개잡주라 저케된건가|0 +유학녀는 걸러야제~~|0 +싹다 싸잡아서 낚아버림.|0 +님이 델따 쓰삼|0 +이번에 금리 오르면 줄줄이 한강 가겠구만 ㅋㅋ 곡소리 좀 나겠네 한국인들|0 +내용은 그럴싸한데 저런 환경이 만들어질 확률 10퍼도 안될 거 같은데ㅋㅋㅋㅋ순딩순딩한 여고딩들 치마 밑에 들춰보는 것도 성공확률 그렇게 안 높은데...|1 +태국에서 악어꼬치 먹었는데 ㅆㅅㅌㅊ임 무시하지마라|0 +"멀봐 이 좀마나 뭐 불만있어?" |1 +너같이 주공사는놈들만 부적합함|1 +그렇다고 휙 나가버리면 에미추랑 문치매만 개이득이지|1 +중딩~20초반이 남자 식사량 제일 많을때지|0 +서로 얘기하고 그러면 웃기겠노|0 + 오사카 부에 존재하는 난치병 소녀 (5)에 대한 지원을 호소하는 가두 모금 활동을 실시해, 통행인으로부터 현금을 가로 챈으로, 오사카 부경 수사 2과 사기 혐의로 오사카 시내의 NPO 법인 「W. S.A "임원 나카무라穣次용의자 (32) 등 5 명을 체포하고 있었던 것이 10 일, 알려졌다.|0 + 예수님이 왕국복음을 선포하셨을 때 유대인들이 예수님을 왕으로 추대했으면 곧바로 천년왕국이 들어섰겠지만, 그들은 예수님을 죽여버렸다.|0 +저게 어떻게 홍콩영화냐 시발ㄴㅋㅋㅋㅋㅋㅋㅋㅋ|1 +“부산 남자“|0 + 굳이 말하면 인도vs좆트남 누가 쓰레기냐?|1 +감염자가 1400만일 수도 있어요.. 사망자는 그보다 밑이고|0 +본사 자체가 성의가 없네 ㅉㅉㅉ|1 +미국에서 현재 리콜 떳음.펠리세이트 에어백 싸이드 쪽 윈도우 불량임.가성비가 좋은 차라 생각하지 가족 있는 사람에게 추천 할 만한지는 생각해봐야됨. |0 +오렌지 냠냠해야디|0 +장모 그럼 무고죄로 고소하세요!!!|0 +븅신 보빨련 ㅁㅈㅎ나 쳐먹어라 이기|1 +그래 잘한다 계속 칭찬해줘 잘하고있어|0 +늙다리새꺄|1 +속도감지리노|0 +이번총선 100석무너지면 걍 연방제인데 다음 선거는 없어|0 +카라반양반 참... 생각이 있으면 후미진데로 옮겨야지 참...답답하네. 차가 3대인가본데 1대 월주차 돈 내는지 확인하세요~!|0 +머리에 뭐 바른거냐?|0 +막줄ㅇㅂ|0 +초반엔 웅엥웅이 젤 맘에 들었는데 살쪄서 그런지 요즘 극혐임|1 + 니가 본것처럼 걔들은 개꿀빨면서 잘삼ㅋㅋㅋ|0 +미국 확진자 조낸 많은데~ 입국 금지해야 한다고 외쳐야지~ 왜 갑자기 아닥하고 있는거지???|1 +말투부터 썩은내나서 줘패고 싶네 늙다리새끼 ㅉ|1 +통계에 크루즈확진자는 미포함시키고 크루즈완치자는 포함시키는 센스를 보여줬었는데…|0 +나가리 올림픽|0 +참 별 시덥잖은 소리 하고 있네. 교민 돕는 돈이 더 많이 나갈까? 신천지 이 미친 것들에 대구 경북에 쓴돈이 더 많을까?? |1 +무현닮았네|0 +돈을벌자..|0 +10년차정도만되도 500~600은 벌지.|0 +머리와 글로 일하는게 몸으로 일하는것을 더 가치있다고 보는건가? 이렇게 퉁쳐서 판단하는것은 사회가 어케 돌아가는지 모르는 편협한 인간으로 멍청함을 고백하는것이다. 세상에 어떤 직업도 없어도되는 직업도 없고 없으면 좋은 직업은 없다.. 그 직업이 담당하는 기능이 없으면 그걸 네가 할건가? 용접이 없으면 집없고 배도 없다.. 수학 강사가 용접을 하면 얼마나 웃긴가? 자신이 전체를 기능하게하는 하나의 부분임을 인식하지 못하고 세상이 자기 중심으로 돌아가는 중세 시대의 사고관을 가진 불쌍한 중생이다..사회적 지능 지수는 병신에 가깝다.|1 +서강대 나쁜학교라고는 생각 안하는데, 브레인이라고 띄워줄때 좀 그렇더라... 그런데 농어촌..? 오우...ㅋㅋㅋ|0 + 팩트를 이야기하면 일뽕으로 몰잖아|0 +팝핀|0 +원래 컴퓨터 바이러스도 백신이 다 못잡아 내는데 |0 +누가 망한데?|0 +쇳덩이로된 축구화 지원하고 싶습니다|0 +나폴리 .도르트문트, 토트넘, 이정도가 안정권같고 그이상은 못갈듯 가도 자리 없고 진짜 맥시멈은 꼬마나 첼시정도..|0 +갓갓 문프 덕에 주가 빨간색에 1500돌파 ㅋㅋㅋㅋ 주가 1000에서 스타트한거죠|0 +그는 진정 우리편|0 +종교충 새끼랑 비슷한급이노근데 좆쥐폰은 브랜드가 없냐? 갤럭시 이런거 없어?예전엔 싸이언 이런거 있지않았냐|1 +예전에 그런적은 있더라 같은 통신사 기기변경으로 어디tv 홈쇼핑몰 방송했던 제품 tv 홈쇼핑 끝나면 자사 홈쇼핑 인터넷에서도 팔든데 당연히 기기변경으로 구입하고 폰은 반납 안하는 조건까지 알고 사용하다가 폰 액정이 깨져서 급한대로 이전 폰에다 유심 꽂으니깐 폰 안되더라 ㅅㅂ 이거 살릴려면 통신사에 문의해서 살려야 된다나 그것도 만얼마인가 2만원 얼마 내야 된다던가 존나 특이한경우 더라 .. 자세한 내용은 안물어봤는데 내 폰 이전 단말기 기변 등록해서 얘네들이 보조금 같은거 몇만원 빼먹고 제한건가 싶기도 하고 개같은 경우 . 보통 기변 하면 이전폰은 공폰으로 되어야 되는데 등록을 하고 사용하라는 메세지만 뜨고 ㅎ|1 +내 맘 딱 이기다|0 +나의 전도가 한명의 남자라도 구하기를|0 +다음병신같은한자쓰는 미개한좆샌징새끼|1 +쓰레기짓 하는 놈들은 99.99999..%가 베충이|1 +전혀 몰랐네요 좋은 정보 감사|0 +94년생이다|0 +기더기가 병신천지..ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +베스트 가자용|0 +그래야 계속 시켜먹지.|0 +진짜 쓰레기 집합소네 나라를 좀먹는것들이네|1 +일베 보내는 기계노|0 +시대를 너무 앞서가는 물건들은 언젠가 빛을 바랜다는데 |0 +탑|0 +아, 할말이 없는지 이젠 정신질환 이야기까지.구차하네. 뭔 반론이라도 해야지 걍 상대말 보기가 싫어서 눈을 막나보구만.|0 +아니 개최국이 탈락한 거야? 조별???실화냐? |0 +혹시 이게 일베 평균 외모는 아니겠지?설마 이 정도일까...|0 +상황설명좀|0 +ㅈㅈㅂㅁㅈㅎ 그리고 게으른 년 만나면 좆된다|1 + 우파는 더이상 가망없어.|0 +혹한기 유격 포대에서 갖고있는거 존나안찢어져서 물량이 넘치니까 보급계가 지가 가위로 찢어서 수량조절하더라 존나튼튼함|1 +금수저 ㅁㅈㅎ|0 +네 와세다 다닌다|0 +퇴사하고 싶다며?|0 +조선시대때 인육을 안먹었을까 ? |0 +갠적의로 존나지루했음 |1 +추진중임 ㅋㅋ|0 +말씀하신 털은 머.. 청소 열심히 해야죠..|0 +그시간에 뉴스보기도 바쁨.... |0 +지화자~!! 계속 해줘~!!!|0 +팬클럽이 아니라 혐오대상^^|0 +나는 먹어도 결핍으로 뜨던데|0 +막짤 무슨 상황이에요?|0 +소방관 의사 비리가 제일 많지 |0 +일베놈들 헬게이트 열었네? ㅋㅋㅋㅋㅋㅋㅋ|1 +수도권 인구집중 현상이 잘못된건 아니지. 오히려 뉴욕 도쿄 런던 같은 선진국 도시나 중국 대도시들에 경쟁하려면 서울을 더 키워야하는게 현실인데 문재인 운운하며 수도권 집중이 잘못된 현상인양 말하고 잇네.그냥 지방은 자연스레 인구소멸되게 냅두고, 공장이나 농업 생산기지화/ 관광지 개발 등으로 가는게 옳다어차피 지방은 인간이 살수없는, 반 축생들이 사는곳으로 변한지 오래인데 왜 지방을 억지로 살리려 하냐?지방인들도 먹고 살아야하니, 수도권 집중현상을 막아야하니 지방도 균형발전 시키자는 주장이북한인들도 먹고 살아야하니 북한에도 퍼주자고 하는 문재인의 개소리랑 다른게 뭐냐|1 +난 1년 키운 소라게 ㅠ|0 +웃기네 그럼 8이랑 9등급은 뭐가되냐?7등급은 문제를 읽기는 하는거지 뭘 찍어|0 + 좌파득세하겠네|0 +보지만 벌려주면공주 대접 해줄줄 알았나보네|1 +그래서 지금 저출산으로 센징 없애고 있자나 저출산이 나쁜것만은 아님|1 +편집자는 최저시급주노 ㅋㅋ|0 +ㅋㅋㅋㅋㅋㅋㅋㅋ ㅈㄴ웃기네 ㅋㅋㅋㅋㅋㅋ벙어리를 만들어버리네 ㅇㅂ|1 +3줄 요약점 설명 앙망 ㅅㅂ3줄 요약점 설명 앙망 ㅅㅂ|1 +ㄹㅇ 그게 지렸었지 ㅋㅋㅋ|0 +돈암되는일을 지가 선택해서 하면서돈안준다고 꼬장부리는 건 왜 그런거냐?그럼 첨부터 공부 잘해서 성형외과나 피부과 갈 것이지이상한 인간이네|1 +파일 열기로 해야함|0 +목사도 아니잖어 쟤는|0 + 입안으로 절개해서 기구넣어서 광대뼈 깎는거|0 +최소 40퍼도 안나오고 마이너스면? 니가 책임질거니? 아니잖아|0 +네생각은 이러하다..|0 +아이구 얘는 며칠전부터 이상한 소리 히고 있네 |1 +아들놈 그와중에 9급 공부하노마 니 빗살무늬토기, 비파형동검 잘아노?|0 +마누라 얘긴 죽어도 안하시지|0 + 그곳의 일이다 이기.|0 +존예네|1 +오늘도 우리회사 노가다 2명 집에감 힘들거같다고개새끼 노가다새끼들이 편한거 할 생각이나 하고 있으니노가다가 괜히 노가다냐|1 +그리곤 인류는 멸망|0 +https://instagram.com/dubnitskiy_david?igshid=1sb3hwmn0vox2|0 +1경기는 사설만가능 |0 +찍기 운이 없었나봄|0 +요즘개념말아처먹은년들이많아서 여자한테 해주면 젊은자지한테 홀려서 집팔아먹을것같아서 못해주겠음 의외로 미친년들 진짜많다|1 +14년 유입이 개나대네|1 +남조선 노동당|0 +전종목|0 +일로 만난 사이, 이해관계로 얽힌 사이에 함부로 인정을 베푸는 게 잘못이지..|0 +온 가족이 쓰레기네|1 +은혜의 강 교회 목사한테 안수기도 받으면 되겠구만.ㅋㅋㅋㅋㅋㅋㅋ|0 +올해 최저 8500얼마인데 거기서 편의점 알바는 깎고 공사판은 올리고 이럴수는 없잖어|0 +니에미 분쇄육 비둘기들이 쪼아먹는중이노 맛잇노 이기 !!|1 +영구입국 금지시키세요.|0 +딥웹 접속가능한 사람이 26만이나 있는사이버 범죄집단 대한민국 ㅎㄷㄷ|0 +저는 하울의움직이는성 좋아하는데 다른것도 만드시나요|0 +고금석,김동술은 사형 집행함|0 +저의자 내구도 존나 의심됨ㅋ글고 바닥에 고정도 아니네대가리가 있으면 저딴걸 설치할리가....|1 +세상은 더욱 더 냉정해져서 본인의 능력으로 가능한 시대는 |0 + 아니 니 근거란게 있을꺼 아니야 ㅋㅋㅋㅋㅋㅋㅋㅋ|0 +토나온당|0 +싸고 왔는데 시팔 또 한발 뽑으러 갑니다|0 +왜 또 조씨냐.... 에혀....|0 +기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? 기사글은 링크 없는게 국룰이냐? 기사글은 링크 없는게 학계의 정설이냐? |0 +닉값 ㅍㅌㅊ|0 +병신 빡대가리 반일국뽕 수준 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +ㅇㅂㄹ|0 +빅근혜때문에 끊었다가 뮨재인때문에 다시핀다 ㅅㅂ|1 +급발진아닌가?|0 +니친구가 사장아들이면 가능함|0 +Aloha~|0 +마지노선 교대|0 +난 효과있는데|0 +전 이렇게 다양하게 여러봉 사시는분 뵈면..|0 +이성계가 다 처리했으니깐 안심하라구!|0 +ㅋㅋㅋㅋ |0 +어딜 가는데?|0 +ㅁㅈㅎ|0 +@기억1970 따땅.|0 +씨부럴새끼들... 주어는없다|1 +이소라야. ㅎㅎㅎ|0 +짱깨는 진짜 존나게 졸렬한나라인건 틀림없음 |1 +친구동생중에 이정×이라는 게이머도 있었지만 암튼 야는 좀 실패 흠 ㅡㅡ |0 +이런 후진형 인간 사진 치워라나라의 주요 인사 모아놓고 한 짓이다후진형 국민을 다독거려 선진형 국민으로 만들어 후진국 탈출시킨 박정희대통령을 본 받아야 한다|0 +당한놈이 병신인 나라|1 +앞으로 어케될지몰라도 지금은 월급쟁이보다 훨씬버는중ㅋㅋ|0 +폰으로안됌|0 +ㅍㅌㅊ를 비싸게 팜|0 +인터넷이나 핸드폰으로 간편하게 성인물을 접할 수 있는데 성교육이나 그런 부분도 더 어렸을때 부터 땡겨서 시켜야 하는거 |0 +잘~~가 세요~~ 잘 가세요~~~~|0 +지디게이 크루한테 담굼질 당해봐야 정신차리겠노이기|1 +아닌척하고 올려야 운영자가 안지우지 병신아|1 +잘했노..|0 +와 멘트 간지난다ㄷㄷ 상남자 성님 멋져부라~|0 +토나올꺼다 당장 내년부터 전기차 요금 오르면 엘미지와 디젤 중간정도 연료비다 |0 +고1때부터일베했다 지금25다|0 +불쌍한 인간 atm들 ㅋㅋㅋㅋㅋ|1 +좆같은 여가부, 무당의료보험, 개정은퍼주기...이런 것만 없애도 사람 죽고사는 분야지원할 수 있지.|1 + 나이묵고 직장 좀 잡고 때 되니 보빨해서 결혼한거고|1 +저분 아니면 수많은 개들 요단강 건넛음.|0 +근데 추도 법조인출신 아니냐?|0 +공론화되길|0 +베트남겨울에도 따셔서 파리모기 벼룩파리 초파리 날라다니냐 ?|0 +국산 클라우드 서비스는 절대로 사용하면 안 됨왜냐면 200% 들여다보기 때문임나는 메일 서비스 같은 것도 국산 절대로 안 씀|0 +예전에 이 분 삶은 뉴트리아 먹방 있었는데 안 보이네. 겁나 맛있다고 감탄하심 피디도 맛있다고 인정|0 +사망진단서는 왜 우한폐렴에 의한 사망이라고 나와있었을까요 혹시 아시나요??|0 +민주주의속에서 독재하는 무지한 학부모들!|0 +강남 같은 교육인프라 공공이든 사설이든 교육시설 인적인프라 공약과 도로확충? 이거는 말그대로 개발을 통한 부동산 상승 집값 올려주겠다는 딱 지수준 같은 얍삽한 공약이군 교육이 먼저냐 아님 자신들 재산 불려줄 나경원이냐?|0 +아주대의료원장이 적와대에 연줄 있으니까 저러는거 아니냐이국종을 저렇게 건드릴 정도면|0 +닌잘알|0 +환경점검 공무원들이 다 그렇지 뭐ㅋㅋㅋ경찰들도 업소삥뜯는게 일상|1 +공산국가 출신들과 겸상하는게 아님.|0 +뇌가 우동사리|1 + 리얼 투자 주주들의 재산을 은행 이자보다 훨씬 증식 시키는 근로자 개개인의 능력이 있을 때임.|0 +없어진 나라 왕 아니냐?|0 +갑자기 시애틀 하니까 생각 났는데 그 전당포 노인네 ㅇㅂ 이제 안오냐?|0 +대구경북은 원래 병신이고 광주는 피해자라는 전라도 출신 백은종... ㅋㅋ|1 +펭수따위 |0 +ㅎㄷㄷ|0 +급사도 좋지만 |0 +한번 쳐야지|0 +근데 그말도 일리는 있어외노자들 일하는 곳이 주로 중소 제조업 공장인데금형 프레스를 예로 들면 숙식비 떼면 남는 돈 100따리야 최저시급도 월급 환산하면 지금 180정도 해일은 하루 12시간씩 힘들지 위험하지 (손가락 없는 사람 많다)비전은 있나? 최소 공장장은 해야 먹고 살만한데 고작 몇년 일해선 못해그렇게 벌어서 언제 공장 차리냐? 사실상 노예 계약이지당장 공무원이랑 비교해도 돈도 더 벌지 대우도 좋지 안정적인데굳이 사서 고생할 필요가 없으니까 기피하는거야내말 틀린가 유튜브 검색해서 댓글 한번 봐라 다 하지 말라고 하지ㅋㅋ 안하는데는 다 이유가 있는거다외노자야 몇년 일하고 자국 돌아가면 그돈으로 평생 놀고 먹고 살지만한국인은 수지 타산이 안 맞아 |0 +제발 ㅇㅂ틀딱보다 니 애비애미한데 신경 쓰라밖에 나오면 니 앱앰이 젤 무식하다|1 +버닝썬이 중국자본 통로였다고 하더만|0 +우와 이제 무슨일 해?|0 +저사연속 아이도 25개월이면 갓 두돌지났으니|0 +이럴때 도와주고 우리어려울때 으~~~~리 요구하는거죠|0 +츄오대 클라쓰 ㅋㅋ|0 +응디시티 틀기|0 + 고발 씹가능|1 + 대답은 하고 자라 븅신아 ㅋㅋㅋㅋㅋㅋ|1 +밑에껀 꼴리는데|1 +우왕 굿ㅋㅋ|0 +회식하고 술취했을때 차태워준다고 |0 +뿌렸고 그걸 보고도 즐긴 놈들입니다|0 +아니 양궁선수가 왜|0 +온도조절 기능 = 노숙자 선점|0 +애쓴다 병신새끼들아ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 + 국회의원 고위공무원 교수 판검사 의사 군장성 심지어 돈많은 연예인 자식들 죄다 원정출산이나 유학보내서 미국 시민권자만들어 놓고 한국 미국 왔다갔다 개꿀빨면서 사는게 현재 한국 최상류층 들이 사는 방식이고 ㅋㅋㅋ|0 +그건 면상이 빻앗다는거지|1 +모자가 삼청교육대 함 다녀와야겠넹|0 +영어하는 놈들이 왜 자살이야|0 +호랑이 저건 새끼라서 그렇지 성체가 되면 100% 지가 처먹음.원래 맹수들도 털 복실복실 할 때 순수하기 때문에 친구가 됨.사냥감을 인식하기 시작하면 ㄱㅐ씹죶간이고 뭐고 지 눈에 안보임.|1 +하던데로 구라 구라 구라|0 +바닥 쓰레기 ㅠㅜ 그때 느꼈죠... 세상 어디에나 땟놈은 있고 또 어디에서나 민폐다.|0 +이새끼가 판매자일듯|1 +이마니 존마니|1 +대구/경북/신천지/새누리/호연일체|0 +먼 소리야 ㅋㅋ지가원한다고 저기서 누가보내주는데?|0 +뚝배기 따서 질본에 전시 찬성!!|1 +50초짜리로 퉁치면 안 되지이이이이잉|0 +조건은?|0 +그 홍어가 하는말 ...나라에서 밀어주니까 큰거지 나라에서 밀어주지않았으면 제대로 못컷다고 하더라 ㅋ|1 +복귀한다고 기사 떴는데 불러주는데가 없는거 같음|0 +엄치척!|0 +그런거 관심 없고 코 성형은 한거 맞지?|0 +이거 모르는 사람도 있나? 모르는 벌레만 있을뿐이지...|1 +귀여워|0 +저기서 환대만받아서그렇지 5년말살고왔으면|1 +저렇게 말하면 와 그렇구나 하면서 진짜 그런줄 암. 개돼지 다수는...|1 +전라도네|0 +딱봐도 남이 인증한거 파온거고 본인거라고도 안했는데|0 +ㄴㄴㄴ 안돼. 돌아가|0 +그러게 나처럼 우측에서 좌측으로 전향하면|0 +사업실패충인갑네ㅋ|0 +갑자기 씹창렬됨 개새끼들|1 +난 눈썹 하나 깜딱 안할끄다.|0 +저건 시대상도 생각해봐야함 일본 애들의 경우 70~80년대고 한국의 경우는 21세기가 되서야 환상이 꺼진거임. 불과 10년전만 해도 서양뽕 일뽕 장난 아니었음|0 +전라도 홍어들이 들어가도 되는 건물은 오로지 게토와 물 안나오는 샤워실 뿐이다|0 +근데 그건 알아라 너학력인증 올리는 순간 너보다 더센놈들도 낄낄 거리며 대기하고 있다는 사실을 ㅎ|0 +지원한거 지들끼리 나눈거 아니겠지?|0 +컨텐츠 개발에 결정적인 기여를 한것도 아니고 단순 편집작업을,애당초 그정도 생산성, 그정도 가치창출밖에 못하는 직원을 그정도 주면 됐지,머 보람튜브가 30억벌면 그 밑에 직원은 월천씩 받아가야 되냐?지분쯤 나눠줘야 되냐? 어차피 남인데? 가족도 아니고.100억을 벌어도 돈 아까운건 아까운거다.꼬우면 유투바하던가, 직접차려 사장이 되던가|1 +지디게이 죽여 없애야됨|1 +개좆평타치|1 +이제 가망없는 나라|0 +이게 뭔,,, 개소리야... 아오...|1 + 내가 그런 말 했다는 근거 가져와 ㅋㅋㅋㅋㅋㅎㅎㅎㅋ|0 +저때까지만 해도 찬스박 무죄받기 전이라 삼일한 드립 개꿀잼이었는데 이제는 삼일한 단어 자체를 보기 힘드노....ㅠㅠ|0 +구글 뉴스로 보세요...|0 +아, 이 형(사실, 나보다 한 살 어리지만 형인 걸로..)은 뼈를 몇 개나 분질렀을까?|0 +역시 대일본 역사가 최고로 재밌단 말이야.|0 +비밀번호현관 의미1도없다 ㅋㅋㅋㅋ 남 지나갈때 따라들어가마ㅓㄴ끝|0 +인도는 왜 저모양이냐|1 +저련년 방생하지말고 평생 데리고 살아줘~~부지런한 보빨병신새끼야.ㅋㅋㅋ|1 +ㅣ고향이 홍성인데 나도 쟤 가르쳣던 선생이 울담임이였던적잇음|0 +나다은 혹시 삼중스파이 아니였을까??|0 +니가 이야기 하는 막강의 근거는 뭔데? ㅋㅋㅋㅋ원하면 사람 세울 수 있고 협조 안하면 공집방으로 막강하게 제지할 수는 있지만 그거 걍 귀찮아서 안하는 거임 - 이게 무슨 개소리지?|1 +꺼억이나 린가드한테 나가는 주급이 아깝지 이런건 안아깝지 굿굿|0 +이불킥!|0 +콧볼넓당|0 +씨발,, 존나 이쁘네,,|1 +주식안함 몸갈아서 버는거라 한강은 안감 산재로 뒤질수는 있음|1 +한번이 어렵지 두번은 쉬운 거 아니냐? 배신자는 받으면 안 됨|0 +부연이 일베하노?|1 +30년전이면 일본 리즈시절임ㅋㅋㅋㅋ 일본이 얼마나 대단한나라냐면 25년째 성장이 그대로인데도 아직도 영프독레벨은 유지함ㅋㅋㅋㅋ 90년대 중반까지만해도 미국을 넘냐 못넘냐 했었는데|0 +와 옆에 톰브라운아야? 좋겠다 나 한 번도 못 입어봄|0 +택시 이미지가 진짜 엄청나다 이기야..|0 +애드립에 빡친건 대본이 아님|1 +가짜 메시는 매시 마누라 따먹을 수 있노|1 +방송이 장난인가 ㅋㅋㅋ당연히 그런 발언은 사석에서 몰래해야지구분 못함?|0 +실업급여 허위로 타먹은적 있는 새끼들은 돌 던질 자격없음|1 +오이나 가지로 쑤시는건 여자애한테도 쉽게 가르칠 문제는 아닌데 동성애에 대해선 너무 나간 성교육이 아닌가 |0 +컬리고 됬을 거라는데서 힘이 느껴지네.|0 +보험 설계하는 동창이 실형 살고 나왔는데 |0 +그런 내용은 모르겠고 새벽한시만되면 wbc앞에서흰색 페라린지 포르쉐지 끌고다니는 씨발것이나 좀 디졌으면 좋겠다|1 + 암튼 좀 미련이 남아있다면 너나 와이프 둘중에 하나가 어학연수라도 하면서 직접 경험해 보길 추천|0 +저대로 되면 성당에 십일조한다|0 +천사가 저렇게 사진을 찍는다고?ㅋ|0 +베들홍들|0 +오디오를 꺼놔서 창문열고 떠들었지만 아무런 반응도 없이 가더라구요..ㅋㅋㅋ|0 +바로옆에 장실이노 ㅋㅋ|0 +빰빠바바밤 빠바바밤 빠바밤 빠바밤 빰 빠바바밤 퓨슝퓨슝퓨슝|0 +한국 역대 사이코패스중 한명인데 의외로 언론에서 안다루더라 ㄷㄷ|0 +님 생각이라면 대구시민 봉쇄해서 다 버렸어야지..정말 이기적이시네..|0 +김대중 노무현 개새끼들은 ㅡ문재앙 이 씨발 개좆같은 새끼 등장이후로되려 선해보일 정도라서 없어진듯문재앙 씨발개좆같은새끼|1 +일본은 중견기업, 대기업이 한국보다 훨씬 많아서한국은 586이 은퇴해도 저렇게 될거같진 않음|0 +너 스스로 비약이 있다고 했잖아.이것도 비약이 있는데?자극적으로 하는 방송의 기준이 뭔데 정성적인 수치는 주관적인 견해에 따라 달라지는건데 사람마다 생각이 달리 있는데 뭐가 자극적이며 아닌지 어떻게 알아.폭력, 패드립에 관해서는 방송에서 제제 하는데 더 나아가 뭘 더 제제한다는거야?그리고 술 담배에 관해서 19금 해야하고 과도한 노출도 못하거나 19금으로 전환 해야 되는데 뭘 더 바래?그리고 너가 지금 억지 말하는데 축구는 공을 차는거지사람 머리를 차는게 아닌데.그건 공이라는 사물을 차는거지 생명체를 차는게 아닌데메이플은 가상의 생명체대 가상의 생명체로서 무기를 이용해 죽이는 거고.그리고 인방도 같아 인방 보고 사람 때리고 문제 일으키는건 그새끼가 정신병이라 그런거야.|1 +휴... 꿈의숲 인근 safe|0 +틀니 2주간 압수|1 +그럼 나이50넘어서 협력업체같은곳 못비비면 어디들어갈수있을꺼같노?대부분 자영업하거나 귀농해서 소박하게산다..|0 +맨날 랜에서만 틀딱 죽인다 때린다 지랄 염병 병신 새끼들좀 실행 좀 하고 아가리 털랑께 ㅁㅈㅎ|1 +지이이이이이잉|0 +저게 요즘 유행임? ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +얼굴이 점점 살찌는거같노|0 +전라남도 순천출신 김웅 대통령 가즈아!!!!!!!!!박근혜 구속시킨 윤석열 빅영수 특검들은 차기 차차기 대통령 가즈아!!!!!!!!|0 +뭔일하길래 이민갔냐|0 +크루 단위로 일일히 사과하게 해야 함|0 +쓰레기는 쓰레기통으로! 고생많으십니다!|0 +근데 저 말이 너무 짠하다. 본인도 너무 안되니까 진심으로 나오는 소리같아서 좀 짠하다.|0 +대구 시장 권영진 하는 꼬라지 보니깐|1 +개종자들|1 +조씨네요|0 +성인 되서도 부모님한테 반말 하는 사람이 있긴해...?|0 +난 씨발 고양이새끼가 왜이레 좆같지?|1 +찐한테 감정이입하는 찐들...짠하다 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +기사 하단의 좋아요 싫어요 슬퍼요는 의미없지않냐?존나게 슬프고 가슴아픈 기사의 내용이지만, 이 기사를 메인으로 올리고싶다고 판단하는 새끼들은 그 슬픈기사에 좋아요를 누를테니.ㅄ같이 기사 하단 조아요시러요 같은 논리는 적지말자.|1 +삼성 지하실에 외계인 있냐?|0 +쪽국 올림픽때까지 계속해~~~~|0 +딸한테 아빠 명기 여친 있다고 자랑 좀 하지 그랬어|0 +미국에서 수입해야하는데 현대노조가 반대해서 fta 때문에 세금도 추가로 안붙음|0 +ㅋㅋㅋㅋ시발 이새끼 존나 웃기네|1 +침대 포기해야하고|0 +즐라도 사기꾼 출신이라던데|0 +마약도 돈내고 정당하게 사는거긴 하지.|0 + 근데 7등급은 그냥 공부 안 하고 시험 차는 애들인데|0 +얘쁘네|0 +참고로 여기서 매달리면 조온나 씨발되는거임많은 찐따들이 여기서 멈추질 못하고 존나 들이대다가 이불킥거리를 만드는거다. 난 다르다고 생각하지마. 열번 찍으면 되는 나무가 아니라 바위산이야.얼른 손떼고 딴년한테 들이대라. 과거 나에게 외치는 심정이다.|1 +미국이란년이 일베에 시세 물어보는거보니 한국와서 팔려나보네질문부터 모자란년인거 존나 티나노|1 +우리는 .... 더 한 것들이 있어서 |0 +대통령님 잘하시다가 또 저러시네..|0 +용접공들이 절대 배우자로 못만날 여자긴 하지 ㅎㅎ|1 +THEK 종로구 창경궁로 |0 +아임 쿠알라룸푸르 채널 안열고 뭐하노|0 +인간관계도 가벼운게 많아진듯함.|0 +자연적으로 저런상이 있다.|0 +입이 왜케 곰, 원숭이마냥 튀어나옴|0 +니 뇌피셜이다. 병신아~힘든일을 싫어하고 자시고의 문제가 아니다.매형이 공장하는데, 추가작업이 필요해서 수당을 줄테니 나와달라고 하면...항상 한국새끼들은 한놈도 안나온다는게 팩트다.안그런 애들도 있지만, 삶의 질이 어쩌구 하는 병신같은 소리에 혹해서 대충 일하는 애들 부지기수다.이런 새끼들과 백수새끼들이, 힘들게 돈벌어서 부자된 사람들 욕하며 '부의 재분배'를 해달라고 지랄하지.|1 +지랄하네 ㅋ|1 +She is korean SCV|0 +잦도 1도 읍쥬~|1 +중국 여자 보는게 레전드네 ㄷㄷㄷ 뭔가 홀린거 같은데 표정이 ㄷㄷㄷ|0 +10등급이다 |0 +센징들이 이게 문제임ㅋㅋㅋㅋㅋ 몇 살에는 뭐 해야 되고 몇 살에는 돈 벌어야되고 지들끼리 나이라는 낙인을 찍고 남들 눈치보고 삼ㅋㅋㅋㅋ|1 +이제 딴거 치러 갔나?|0 + 차값외적인부분에서도 3시리즈탈빠엔 안타는게 맞다|0 + 전기로 배터리 충전하면 전기차|0 +3. 예수님이 내 죄를 대신해 돌아가시고 부활하셨다는 것을 인정할 것|0 +아랍제국은 아라비아반도고 그 아라비아가 흡수한 문화도 페르시아가 원류. 이집트는 5000년이상 세계 최강국 그리고 한국은 한번도 세계를 주름잡은적이 없음 근데 뭐어쩌라고?|0 +아무나 함 ㅅㄱ|0 + 직장인 안만남|0 +홍어 쉑 만날 ㅈㅈㅂ만 처올리노|1 + 공부 못하는 놈들 성적 올려 주는게 선생 역할이지|0 +나베가 35%라는게 말이나 데능교.|0 +ㄹㅇ 지들이 쓴 소설대로 간거아님?ㅋㅋㅋㅋㅋㅋㅋ|0 +매크로 방지하는거잇지않냐 그 신호등이나 횡단보도 구별하기 그런것도 뜷리나|0 +마리텔에서 다룰 주제도 아닌데 mbc심각하네|0 +그래도 베트남에 비하면 중국은 선진국임물론 중국도 미개한 나라지만 베트남이 그 정도로 더 미개하단 것임최소한 중국은 대놓고 공무원이 뇌물 요구하진 않는데 베트남은 아직도 대놓고 요구함|0 +하 개삘받았다 내일부터 유치원 간다|0 + 속으론 씹새끼다 생각하는데 겉으로는 그대로 좋은 사람이라고 칭찬하는거지|1 +피의자가 억울하다고 하면 분노해서 중형|0 +정미경 출판 기념회에서 만나서 사진 같이 찍은거구만 |0 + 캠리 하브 2.5 가격 = 그랜저 하브 2.5 가격|0 +미친차주????|0 +섭외가 좋은게 아니라 정신나간 국가 홍보사업으로 돈쥐어주고 쟤한테 맡기는거임 이래서 개 돼지나라다|1 + 로마서 11장 22절|0 +여자랑은 무조건 거리를 둬라 미칠듯이 눈에 띄게 거리를두고 직장상사가 너무 거리를 두는거같다 이런말나올정도로 일부러 거리를둬라 좆된다.너무거리두는거같다 이러면 그냥 재가 여성분들앞에서는 좀 잘안되요 허하 하면됨|0 +그래서 진글 퍼오는건 재미로 충분하다는거|0 +저 벤치들어오면 공짜충,노숙자,틀딱,김여사 전용 나와바리됨|1 + 물론 사기업보다 공무원은 절대 저렇게 해선 안되지만|0 +그래도 삼성 부장 정도면 연봉 몇억씩 받았을텐데 그리고 퇴직금도 오지게 받았을텐데 그거로 차라리 일용잡부 뛰면서 한달에 200정도 생각하고 나머지돈으로 생활비쓰지뭣하러 떡볶이집 오픈해서 고생하시냐.... 진짜 장사로 투자1.5억해서 1.5억 본전만 찾아도 다행인시대다게이들도 만약에 퇴직금 다해서 현찰 2억 정도 있으면 그돈 생활비로 야금야금쓰고 일용잡부 뛰거나 아님 뭐 월급쟁이 마트 직원 그런거라도해라어지간해서 사업하면 돈 까먹고 싹 다 망하는 세상임|1 +우크라이나 곡창지대|0 +그래도 인강강사면 자중했어야 하는게 맞는거였어|0 +bbc써있는건 빅블랙콕인가|0 +쪽지보내주세여|0 + 만인에게 평등한법.|0 +ㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷ|0 +그거와 마찬가지지|0 +통상적인 배치인데조질려고 준비라니 그러니까 적화통일 이야기나 하지.|0 +울엄마랑 이모 고모 사촌 누나들은 다 한국당이야|0 +저런 영상이었구나뒤에서 암살자 튀어나오는거 첨 알았네로그인화면 재생을 항상 꺼늏다보니|0 +대기업 중소기업 임금 비슷한거 인정|0 +이새끼한테 낚이지 마라 이교수를 이용해서 사회주의라고 이름만 바꾼 공산주의를 찬양하고 공산주의화하려는 반동분자다|1 +씨밸년=틀딱용어ㅁㅈㅎ|1 +버섯돌이가 떠오르네요......|0 +코노야로!!!!!!|0 +니 와이프가 42살에 애낳았다고?|0 +링크줘봐|0 +밑에 뭐라고 쓴거냐옵빠이X라세떼 구다사이|0 + 서버는 php랑 mysql 책사서 공부하면서 만듬|0 +아 씨~ 또 속았네 ㅋㅋ|0 +팬티..꼬쟁이..|0 +다들 커밍아웃 하느라 바쁘구나|0 +학생들만 상대하면 시야가 |0 +노링크 ㅈㅎ|0 +어차피 도울 수 있는 방법도 없을 듯한데요|0 +거의 그렇지 ㅋㅋㅋㅋㅋ 근데 나도 귀찮아서 뭐 대단한건 안 함|0 +우버가 4위에 껴있는게 웃기네ㅋ|0 +좌파라서 좋아하는 사람도 있지 않나?|0 +지랄 자동으로 하지|1 +닌 어딘데 중앙대무시하냐 확 쑤셔불랑게|1 +시발 넌 4번죽어라 |1 +희생하시는 분들 응원은 못 할망정 |0 +지가 ㅂㅅ인지|1 +용접하면 정자 죽어서 할꺼면 빨리해야지 늦어야 서른살초반|0 +레토나크루져 타고있는 1인입니다 ㅎㅎ|0 +그래도 대구는 정부욕할듯.|0 +돈 많고 체력 되면 비번휴무휴가 겹쳐가지고 존나 여행 다니지 ㅋㅋㅋㅋ|1 +남자가 미친 거 맞는데...이 파혼은 여자한테 축복인 거임. 짐승한테 과몰입하는 정신병자들은 걸러야 함.|1 +는 개뿔이고 시발 그냥 누가 더 그럴싸한 거짓부렁 잘 푸는지 아무말 대잔치 경연대회였음..|1 +아니니까 안심하고 생활해라^^|0 +그전에 진흥왕이 한강먹고 백제 뒤통수 후린거 아니었노|0 +9월달부터 기출하고 정승제 무슨 기출강의듣고 평가원기출 프린트해서 실모풀듯이 파이널준비하다가 수능보러감|0 +북유럽 농사가 우리나라보다 낫다는거냐? 추워서 농사 안될거같은데...|0 +비디오 있어도 안에다 싸는 장면만 없으면 그럭저럭 참을수 있을것 같음근데 만악 안에다 싸는 장면까지 있다면 멘탈 나갈것 같음|0 +아오 시발|1 +99.9%이상의 여자가 저렇게 생각하고 있는게 현실|0 +이해가 안되는게 사회주의가 이론 상으로는 아름답다는 대가리 총 맞은 좌빨새끼들이 할 만한 병신 소리는 왜 자꾸 재생산되는거냐 그것도 우파축에 든다는 일베에서? 확실히 우파가 제대로 망하긴 했구나일한 사람 사유재산 뺏어서 모두와 공평하게 나눈다는 발상 자체가 실현에 실패할 수 밖에 없는 덜 떨어진 이론이고 설사 실현가능하다고 해도 가난하니 가진사람 사유재산권 침해한다는 것 자체가 윤리적으로 옳지 못한다는 것은 자각도 못하는 인간조무사들이 존나 많긴하나보구나당장 자기집 도둑질 당하면 방방 뛰며 악쓰고 울 것들이 자기보다 잘 사는 타인의 사유재산권 침해는 당당하게 외친다. 모럴 자체가 붕괴한 한국인이니 윤리적으로 옳지 못하다고 해도 감흥도 안되겠지 그치? 뭐 이번 통계보니 10명 중 4명은 세금 기여조차 못하는 인간조무사들이라는 것이 공식 증명 됐으니 당연한가 싶다. 현실에서 실패하고 이론도 병신같은게 사회주의다. 투표로 도적질을 정당화 할 수도 없고 이론이 아름다운게 아니라 멍청한 개소리라서 실패한거다.사회주의 이론상으론 아름답다는 개소리는 다른 덧글서도 아무도 안하길래 이것만 적고 이국종 교수의 행동이 사회주의라고 주장하는 멍청한 원글 주장은 다른 덧글서 이미 많이 말하고 굳이 반박할 가치도 못 느끼겠으니 여까지만 적는다.|1 +솔직히 울나라에서했으면 개꼴렸을듯...|1 +진주 하동 사천, 고성, 함안, 밀양은 중간색)|0 +수사지휘를 검사가 안하고 경찰이 독단적으로 할 뿐이지 기소권은 아직도 검사가 독점하는거 아니냐?|0 + 평소에 애미나 아줌마들이 너한테 잘생겼다 하는거 진짠줄 아는데|1 +ㄹㅇ 발끈해야되는건 진짜 용접명인들이나 잠수용접사 같은 귀한분들이지일당이나 받으면서 일하는 김씨새끼들이 왜 발끈하냐? ㅋㅋㅋ|1 +빨갱아 반일에 대해서 어떻게 생각해?|1 +터부시 = 금기시|0 +관광객이겠지|0 +방구냄새 맡고싶노|1 +테슬라는 조립품질가지고 이야기 하는게 아니라 거기에 탑재된 자율주행 기술에 역점을 둬야함.. |0 +존나 오래전에 본거네 ㅋㅋ|1 +이런 씨발련이.... |1 + 그러니 한국당 지지율이 시궁창이지ㅋ|1 +제떨이도 해라네 제랑 해버려|0 +산송장새끼 발꾼하는거보소 ㄷ|1 +돈써가며 또 보빨짓하겠지ㅋㅋ 요새 그런년 많다 받을거받고 섹스나할라고 만남. 그돈 지딸한테나 쓰지 ㅉㅉ|1 + 일본인들도 잘 모르는 듣보잡 강소 소재 기업이 많음. 그래서 공돌이도 취업이 잘돼.|0 +저때는 별거아닌 떡밥이 사찰넷 떡밥 같은거였음 땡중이랑 노는거 개꿀잼 이었는데 |0 +조형기 5 년받았는데, 1년 살고 보석으로 나왔답니다|0 +ㅡㅡ 버티는 놈이이긴다|0 +분위기가 안 좋아서 사려다가 돌아서는 사람도 있었고요.|0 +개소리 쳐하노 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +주행느낌은 개취의 영역이라 그런겁니다 다 달라요|0 +일본자국내의 감염원이 공항 항만을 통해 홰외로 나가는걸 막는 차원이 아닐까?|0 +ㅎㄷㄷ 하네 과거...|0 +국가의 미래가 불투명한 상황에서 유신선포하고 자주국방에 총력|0 +미안한데 기술과 기능좀 구분하자|0 +인정할건인정해라 닥그네 쳐빨지말고 |1 +아베 간바레! 이번 올림픽 미루지 말아줭!|0 +뭐가 지겨움 여러가지 사건에 대한 인증인데|0 +저게 멋있냐? 난 저거 보면 할배가 큰사이즈 패딩 입은거 같이 보이는데|0 +확실히 이쁘긴 이쁘다 ㅇㅂ|0 +진짜 좆같은 년들만 골라놧네|1 +오를때 사야 그게 싼거였지|0 +슈마훠 미친ㅋㅋㅋ|0 +백인 흑인 황인은 외모로 구분짓지 그럼 뭘로 구분짓냐?|0 + 그구절을 정확하게 해석 해줄께....|0 +정답 : (빤스목사) 가 (헌금) 을|1 +으휴 찌질한 새끼..뇌가 신선할때 일을하든 공부를 더 하든 뭐라도좀해라 일베와서 댓글로 저게 자랑이라고 저딴 뻘글 쓰면서 칭찬받을라카지말고|1 +보험회사 하이애나 새끼들. 과실 어떻게든 잡으려고 하는게 느껴지네요.|1 +야 왜 아직도 탈출 안했냐|0 +시발 좃같다 ㅋㅋ|1 + 더 뒤져야해 개새끼들 대가리 다 박살나라 |1 +링크한번 딱 주면...|0 +찐따들이 일본가서 살고싶어하는거자나|1 +어우씾소름|1 +양심에 안찔리나?|0 +결혼생활에 환상가지는 애들 이해가 안감 좋은 점만보는것 같은데 요즘 사람들이 얼마나 개인주의적긴데 요즘은 가족도 옛날의 그런 이미지가 아님 정서가 완전 달라서 |0 +뭘믿고이렇게까지한다고 생각하냐 ?개표기 , 사전투표 |0 +원숭이 사쿠라 핑크당의 자위대 간부!|1 +꽈자와자꽈꿍꽐라까자 '쾈' 자불라자|0 +쳐 자라|1 +클럽 보다 나이트가 더 쉽냐?|0 + 투르크계민족은 황인임|0 +씨발 만베받은것중에차렷아재 아들하고 김대중하고 싸우는거파이날판타지게임을 이용하여 합성한거그거 부캐동원 7개 ㅇㅂ준거 생각나네|1 +여자살인범들은 거의 비슷한게 있는데 수면유도제 아니면 수면제 농약 청산가리등 독극물이 주로 사용된다는점, 우리나라 최초 사이코페스점수 만점을 기록을 기록한 엄인숙이라는 여자 사형수가 있는데 정말 잔인함, 우선 무척 미인었다고 하고 순진하게 생겼다고 함, 그걸 이용해 남자에게 접근 수면제를 먹이고 잠이들면 바늘로 눈을 찔러 장님을 만든다고 함, 그다음 일부러 자빠뜨려 뇌진탕을 만들고 식물인간 상태가 되면 뜨거운 기름을 얼굴에 부었다고 함, 그런식으로 상해보험이니 사망보험금이니 타먹으려고 자기 엄마까지 눈을 바늘로 찔러 장님을 만들고 보험금을 타먹은 희대의 싸이코페스년이었음|1 +그걸 억지로 가동하다가 원전사고 내고 싶습니까? 월성원전 배관과 원자로를 싹 다 갈아엎고 가동할까요?|0 +반박해주고 싶은데 0렙이라 분탕 가능성 높네 앤 한국당 의원 구성원 보고와라 ㅂㅅ아|1 +공적마스크 잘 하고 있는것 같은데.|0 +ㄱㄴ|0 +멤버십은 뭐하는거?|0 +지성인이다|0 +네이브가 어디야?|0 +ㅄ 누가 고백을 카톡으로 하냐|1 + 보다 훨씬 나쁜새끼들인데 말이죠....|1 +넌 키존나작지? ㅇㅇ 나 190임|0 +하나같이 다 개좆같이 생겼네 pc주의자들이 저런 짓거리 앞장섬|1 +싫다 싫어 꿈도 사랑도|0 +지능이 낮은게 아니고 빨간놈이라서 그런거지|1 +네다음고졸|0 +암묵적으로는 알고 있지만 다들 말은 못 하는 거니까|0 +근데 일은 존나게 할꺼다|1 +보기 좋네요~~~|0 +그냥 20대 허세 한1남충과 20대 한녀 피싸개들의 콜라보지 뭘 부모탓이야힙합 찐따음악이나 쳐듣고 람보르기니 마세라티 롤렉스 구찌 Flex 이지랄 하고 계집따먹는 자랑, SNS에는 페로몬향수 커플룩 개강코디 골목식당맛집 꼭가봐야할, 꼭해봐야할 이지랄로 남 보여주기 허세문화를 기업들이 나서서 바이럴마케팅 오지게 해대는데 행복할리가 있냐??저기 베트남 캄보디아 가서 낫띠따 응우옌 같은 외노자들 일하는거나 보면 월 20, 30따리인데 혼자서 200쳐벌면 오히려 돈남지벤츠못타서 모델여친못사귀어서 명품옷못입어서 존나 배알꼴려서 인생탓 부모탓 불평불만 존나하는데 너네 부모도 그냥 대가리에 섹스 하라고 조물주가 주입시켜놔서 섹스하고 좆물싼거뿐이다 너무 뭐라하지마라그리고 돈 많아도 갑자기 꼴리거나 몸이아프거나 우울해지거나 등등 결핍이 찾아온다. 얘네는 결핍을 해소하는게 쉬우니까 상대적으로 부러워보이는거지 결국 결핍은 근본적으로 누구나 예외없이 생긴다 인생은 그냥 결핍의 연속이고 그걸 해결해나가는 과정임배고프면 쳐먹고 똥마려우면 똥싸고 섹스하고 앓아눕고 슬퍼하고 그런 과정을 의미없이 반복하는거다인생에 대해 더 살아갈 가치가 있는지 판단해보고 선택 해봐라 ㅇㅇ 받아들일지 포기할지|1 +2차대전 동맹국이냐? ㅋㅋ|0 +개독교가 얼마나 사이비종교인지는일본을보면 알수 있음 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ일본은 개독교가 없는 나라인데 지금까지 미국 다음으로 세계최고 경제대국임 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +꺼져|1 +근데 그 요구권이 법개정으로인해 경찰들이 맘먹고 뻗대면 안먹히는게 문제다. 완전 검찰과 경찰의 수직관계가 이번 검경수사권조정 통과로 인해서 패러다임이 바뀌어버림. 시발 검사들 윤석열 욕 엄청많이하겠는데?|1 +아놔 주식 신발에 걸리겠네|0 +너도 부들거리기만 하고 반박은 못하잖어? ㅋㅋ|1 +넌 같은 지능의 사냥꾼 머리랑 회계사 머리가 같다고 보냐?|1 +개선유무 따지고있는데 서울대는 왜나옴?|0 +ㄹㅇ 거기에 아낌없는 지원을한 따블당의원님ㅠㅠ 든든합니다!!|0 +정신 피폐해지고, 돈도 나눠 줘야 하고, 애까지 낳으면 어쩔|0 +게이야 ㅅㅌㅊ들은 어떻게든 다시 재기하고 |1 +우리 아빠가 아이돌이라니|0 +제대하면 더ㅈ될거임ㄷㄷ 아프리카티비 장악할거임|1 +부캐가 얼마나많으면 이런똥글싸도일베감?|0 +함께 별달러 가즈아~~ ^^|0 +워홀가고 싶다. 유럽 여행가고 싶다. 이러는게 아님...|0 +절대농지로 농사만짓게 했으면 정부가 당연히 책임을져야지|0 +생각해보니 그 많은 개들을 수용할수가 없겠네|0 + 우측은 더이상 가망없는 노답임.|1 +ㅈㅈㅂ드립 |0 +국민배우 정도되는 한석규 정도면 뭐 팬들이 구독해놓고 하루종일 보고 앉아있겠지만 여배우면 무슨 컨텐츠가 있겠어? 이뿌다하고 걍 보다 질리면 나가버리지|0 +얼마전에 일본가는데 옆에 전투기붙던대 ㄷㄷ 존나 신기했음 |1 +눈깔이 사신가;;|1 +좋은 생각은 나눠야지|0 +ㅜㅜ 당분간 버스타고다니면돼..ㅠ|0 +ㅋㅋㅋㅋ개지랄하고있네|1 +인정하기 싫지만 일본의 탄탄한 스포츠 라인업은 전부 애니메이션으로부터 나온다.일본 애니메이션이나 만화는 정말 장르가 무궁무진함.. 스포츠도 어찌 그리 만화로 박진감을 잘 표현하는지..어릴 때부터 만화로 접하는 스포츠나 각각의 장르에 동경을 품고 커서 그쪽으로 진로를 정하는 일본학생들이 존나 많음.그에반해 우리나라 봐바. 죄다 학원폭력물 아니면 판타지, 조폭물.. 씨발 그래서 급식새끼들은 양아치가 널렸음. 정신적으로도 존나 피폐하고|1 +엽문시발 걍 짱깨 쿵푸권사 몆이긴거임|1 +그래서 냉부에서 이연복은 레시피 다 공개하잖아근데 후기들보면 어설프게 따라하다 요리 망친다고하던데 아무나 못한다고|0 +케톤대사가 일어나면 혈중에 포도당 대신 케톤이 돌아다니는거고 케톤이 산성물질이라 혈액이 산성화돼 그리고 저탄고지 결과 보면 전원 LDL이 올라감|0 +맞어 털빼면 욕할거없음|0 +일베 빨렙들 그냥 영입해 그게 낫다 |0 +기본에 충실하면 |0 +나는 글쓴게이 말도 맞다고 본다|1 +아이스크림 포장지 씹에바 아니냐? 누가 포장지를 행궈|0 +부전교회 고등부에 있을때정용식목사가 사랑의 반대말은 무관심이라고 했다.예수님이 원수를 사랑하라고 하니까 학교다닐때 날 괴롭혔던 애들을 좋아할려고 애썼다.|0 +아니 야당은 도데체 뭐하는거야?|0 +비타민으로 물타기를 하고앉음 비타민보단 골구로 균형잡힌게 중요한거지|0 +이대일로 했어야지|0 + 1년째 지하철로 출퇴근중인건데|0 +기본 장애등급 나올거고 소송 승소하면 평생 연금각|0 +ㅇㅈ|0 +6학년이었군요|0 +이승만 각하하고 무슨 상관이노ㅋㅋ|0 +짱깨들의 논리 여지없이 나오네|0 +저거 재밌음ㅋㅋ|0 +지랄도 풍년이다|1 +정말 열심히한사람들은 공정한 세상이라고 한다. 노력한만큼 결국 돌아온다고가슴에 손을얹고 정말 불공평한세상이라고 울며짖을수있는 권리가 있는지 생각하길 바란다 물론 출발점은 다르고 노력한만큼 돌아오지않고 때로는 엄한일도 당하고 죽고싶은일도 생기는게 인생인데열심히해보고 안됬을때 울면서 불평해도 좋다 하지만 시작은 해봤으면 좋겠다! 인풋대비 100%효율은 아니더라도 최소 40%는 나오는게 인생이더라 |0 + 전라도 홍어 씹새끼들이 쪽바리 경찰들과 결탁하여 3.1운동 방해하기 위해 만들었던 단체. |1 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +저사람은 SNS 안하나|0 +헬반도에서는 착하게 살면 안 됨|1 +재난 지원금 야근비로 돌리려다 실패하기, |0 +전 그냥 그런 광고문자는 수신차단해버림|0 +ㄹㅇ? 언제>|0 + 중국,미국 성님들이 손대지않는이상 안오른다|0 +개소리 ㅁㅈㅎ~예전에 전두환 등등을 거치면서 좌파들이 권력층간의 이너서클, 낙하산 등등으로 공격해서권력을 점점 빼앗기니까 이런짓을 못하게 된거지.원래 이걸 안했던건 아님. 전두환 등등은 주변사람들에게 부를 안겨줬지. 근데 그 결과는? 보수의 심장이라던 대구에서도 전두환의 비리 처단하라 그랬음.|1 + 아이러니한게 어릴때부터 영어공부해오면서 좋은 글을 영어로만 많이봐서 영어쓸땐 고상하다는 소리많이듣는데|0 +왜놈 + 왜구 ㅋㅋㅋ|0 +물타기 해라|0 +이거는 곤봉챙겨서 새벽부터 줄설만 하겠네|0 +적어도 민주당 좌파뽑는애들은 잘못된신념이라도 본인신념이라도 있지 자한당지지새끼들은 이노옴 박통이 지옥에서우신다 이지랄중 박정희 전두환 독재자 씹새끼들 외치먄서 자한당지지중 뇌가빈건 우파라는새끼들 아닐까 ㅋㅋ|1 + 애초에 용접공이니 기술직이니 하는 걸 |0 +보따리 장사질 하고 있네?|0 +팔은 안으로 굽는다구 같은빨갱이새끼니까 편드는거 븅신아|1 +9수 ㅋㅋㅋㅋㅋ 된장국이여?|0 +@노가다쟁이 />|1 +유승민 배지다는 게 개혁이고 통합임?|0 +이땅에 진정한 보수가 나와으면 좋겠다|0 +☞☜삼가고인의 명복을 빕니다. 안타깝네요..ㅠ|0 +스타킹 발냄새 노무 좋다|1 +그라체 정치는 서울대 옥스포드 정치학과 나오신 손학규 슨상님 정도는 하셔야|0 + &|0 +극진가라데는 나중이고.|0 +4년동안 몇천 뿌리고 한다는짓이 ㅋㅋ|0 +너무 잘하고 있네요~~|0 +개인의 선택을 너무 뭐라하지말자. 나도 한 때 강사하고 싶었음. 고교 수학은 너무 재밌고 풀이법이 여러 가지인데 대학 수학은 엄밀히 라는 시바것 때문에 잼도 없는데 풀이법 찾으면 공부를 할 수가 없으니.. 애들 가르치고 싶더라 ㅋㅋ 노원에서 강사도 했었는데 학력 속인 놈들 조터는 거 꿀잼ㅋㅋ|1 +뭔 50대야 강호동이 이제 50인데|0 +와 존나 개꿀팁이네..그렇게 하다가 그냥 재결합 안할수도 있는거고 재산은 다 나한테 있고 개씹끌띠|1 +광주에서 일베하다 걸리면 이렇게 되지 않냐? 헛소리 ㄴㄴ|1 +틀딱들 바로 부모님 욕 시전하는것보니 못배워처먹은 세대가 맞긴하나보네....|0 +트럭도 확인하고 길벼주네 ㅋㅋ|0 +키가크면 어깨가 넓어서 165가 제일적당하다고 본다|0 +ㅌㅋㅋㅋㅋㅋㅋ치즈냄새|0 + 샌드백은 쿠팡에서 로켓배송 17700원짜리|0 +뭐 놀랍지도 않음 |0 +요즘 나비탕 잘 안팔리노 각자 개성에 맞게 키우면 되지 뭘그리 홍보질인데 무슨 아이돌빠순년인줄 알았네 ㅋㅋㅋㅋㅋㅋㅋㅋ|1 +나도 여기서 막혔는데 ㅋㅋㅋㅋㅋ그대로 주변을 아끼지 않는 나쁜 사람이구나로 생각해서내용들이 이해가 안 됨|0 +캐치미이프유캔 ㅇㅂ|0 +내가 이러려고 국회의원이 됐나..|0 +그럼 대선때 터는걸로|0 +면마스크의 장점은 빨아쓸수 있고 땀이 훨씬 덜 차고 외피만 알콜 소독이 가능하다는 점.|0 +근데 사재기해서 집에 놔둘 공간이 없음..|0 +스듀 이지영 뺏기고 초상집일건데 ㅋㅋㅋ|1 +저 24렙이 너냐?|0 +좆같이생긴새끼가 화나서 이지랄하면 현실에서 진짜존나처맞지ㅋㅋ|1 +발기부전어떻게고치냐|1 +삼성이 그럼 전라도 사람 존나 뽑은거네? ㅋㅋㅋㅋㅋㅋ말이 되는 개소리를 좀 해라 ㅉ|1 +지잡년이 무슨 씨발ㅋㅋ|1 +아 ㅋㅋㅋㅋ 저 매매혼 사생아 튀기새끼? ㅋㅋㅋ 블라했음 계속 따라다니길래 ㅋㅋㅋ|1 +다벗고 자지 발딱 세우고 서로 쳐 만져 주는 드레스코드인지 나발인지가 개좆같다고 이 씹게이 허벌 똥꼬충 새꺄 ㅗㅗ|1 +동탄에 전라도 사람 ㅈㄴ많음민주당 화성시장이 화성 서부에서 밀리는데동부, 특히 동탄에서 몰표 받아서 당선됨|1 +지랄이 풍년났노? ㅋㅋ ㅁㅈㅎ나 쳐머거|1 +진심이라면 신소율은 1000명중에 1명이다.|0 +이미 성차별을 염두에 두고 있으니 특별히 쓸일이 없었겠지|0 +정자 제공자가 누군데? 그리고 46세에 임신 출산이 가능하냐;; ㄷㄷ/ 근데 46세에도 비쥬얼이 저정도면 나라도 데리고 살수 있을듯;|1 +역겨운 홍어애미뒤진 찌릉내 ㅆ바꺼|1 +수사권 조정안이 통과되었다 하더라도경사 이하 순사들은 여전히 수사의 보조자에 불과한데무슨 어깨가 들썩거리노...이 시각 노량진 공무원 수험가 파티 분위기임?|0 +그렇게생각하니까 진짜네|0 +인두루 지지고 오면 해줍시다~|0 +고도 조절 잘못하면 바로 치킨으로 튀기기좋은 조각으로 변할텐데?|0 +인증은 니애미보지에다 쑤셔박고옴?서강대에서 존홉 ㅋ 밑바닥새낀 뭐가달라도다르다|1 +이과는 50점 정도 차이 날 듯 ㄷㄷ|0 +배우러 왔으면서 도를 넘어서 깝치니까|0 +노예들 다 뒤져서 국가생산성에 차질생길까 줬나게 살리는거 |1 +지디크루냐?|0 +야 너네 저 위에 여기자 경기방송 지금 어떻게 된줄 아냐? 방송국 재허가 겨우 받고 예산 삭감 한걸로 기사나온다. 씨발 빨갱이 문재앙 북한이다|1 +어르신들 표라도 빨기 위한 전략적인 행보에 불과합니다. 저러고 총선 끝나면 자기들 밥그릇 챙기기에 운운. |0 +ㄹㅇ 서점에 혐한코너 있는 나라 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 생각해보면 존나 미친놈들이네 ㅋㅋㅋㅋㅋㅋ|1 +그렇노. 나도 위장 발기 해야겠다..|1 +호아킨 없어서 ㅁㅈㅎ|0 +씨발 ;|1 +통까흥 통빨망|0 +다른사람에게 전염시킨건 아니지...원숭이한테 전염시켰을뿐|0 +위 2는 ㄹㅇ|0 +ㅇㅇ 절대 할게못됨 하고나서 허공에 잔상보임|0 +천생연분이노..|0 +이새낀도대체어떻게대통령자리에오른거지|1 +당시는 개나소나 패스임|0 +그렇게 따지면 중국도 GDP가 세계 탑클래스여...지금은 모르겠지만 10년전쯤은 세계 2위였다|0 +안전 거리 지키라고 그렇게 이야기했는데 그리고 안전 거리 지키면 좀 끼어 들지마|0 +무슨경력이노게이야?|1 +그러면 적자내는 중증외상환자가 더 몰려서 병원은 더 큰 적자를 보는 악순환이지|0 +그 무슨 바람에서 제사지낸다고 템깔아뒀는데 초보자새끼가오더니 그거 다줏어먹는짤 있는놈있냐 개웃기던데 ㅋㅋ|1 +토니 몬타나|0 +잡대새끼 부캐 어서오고|1 +저 희대의 사기꾼과 우주의기운이함께하는분은 그냥 광화문앞에서 돌팔매질해서 보내드리면 안되나요|0 +베충이 시무룩 ㅋㅋㅋㅋ|1 +애미 씨발 그럼 무한도전은 10수 이상 한 애들이 해야하고정글의 법칙은 정글살다 온 애들이 해야하냐 ㅋㅋㅋㅋ아가리 잘털고 시청자 공감 이끌만한 이야기 잘하는 애들이 하면대지 ㅋㅋㅋ|1 +저거 탈모아님 걱정 ㄴㄴ|0 +래미안이라고|0 +후장센세 ㅇㅂㄹ|0 +니네 둘 다 틀림. 길고냥이건 집고냥이건 겨울엔 발정 안함요.. 발정하는 극소수가 있긴하지만|0 +참고로,울나라 현궁도 비슷하게 생겻어요.|0 +단계적으로 규정을 변경하면됨ㅋㅋ|0 +@반일반북 철수형잘있냐|0 +죽어 죽어|1 +뒤집어서 인쇄된것까지 그런아이디어가 어디서 나왔겠냐.이렇게 설명해줘도 이해가안되냐?|0 +진짜임 구라임? ㅋㅋ|0 + 그냥 한 번호로 밀면 나오는 게 7등급임.|0 +할게 없으니까ㅋㅋ|0 +이게정확한진단이지 하이튼 손뽕들보면 대깨문생각남ㅋㅋ 패죽이고싶음 주제좀알지|1 +ㅋㅋ 고백이따구로 하면 망함 ㅋㅋ 찐따새끼들아 고백은 그냥 확인이야 |1 +당연하지 지능수준있으면 애국심이생기겠냐 나같아도 탈조센함 일뽕이될수밖에없음|1 +따뜻한거지 편하기는병신아|1 + 내가 그런 말 했다는 근거 가져와 ㅋㅋㅋㅋㅎㅎㅋㅋㅋㅋㅋㅋ|0 +ㅈㅈㅂㅁㅈㅎ|0 +대가리 텅빈 년들..|1 +제목으로 잡아도 문제아님? 남의 정보를 왜 맘대로 들여다 봄|0 +부모가 되어보니 이글을 읽을수가없습니다ㅠㅠ|0 +80 년대 일본 전성기에 일하면서 쌓아온 자산 ( 딴짓 안했다면 ) 을 꾸준히 저축 했다면 |0 +유튜브를 통하여 '홍익학당 윤홍식님의 강연들'을 들어봤는 데?아주 유익했음.|0 + 다만 자본주의 원리는 인간의 본성임|0 +가끔씩보면 이건 아닌데라고함|0 +공익신고 제출사항으로 1. 신고자 성명, 연락처 2. 위반일시, 장소, 위반차량번호 3. 교통위반 증거영상 또는 사진|0 +ㅋㅋ 얘 귀엽다 개따먹고싶네|1 +디스크가 버텨줄때 오지게 벌어먹고 나와야함|1 +요즘에는 대행업체에서 소방점검을 많이 하기 때문에 저런 일은 많이 줄었지만 그래도 장사 좀 해보려고 건물에 들어가서 시설, 설비 갖추고나면 꼭 점검 한답시고 들이닥치는 소방관 새끼들이 있다두명도 아닌 꼭 한 명이 와가지고 존나 말도 안 되는 걸로 트집을 쳐 잡는다돈 달라는 소리지 씨발 새끼들근데 좆같은건 그렇게 한 명이 왔다간건 정식 점검이 아니라서 이후에 소방점검이 또 와존나 씨발 한국은 장사 한 번 해먹기도 좆같은 나라다|1 +야 1만도 존나많은건데 숫자가중요한게아니라 처벌이나 빡세게 해라 일반야동도 아니고 실제영상을 텔레그램으로 찾아가서 봐놓곤 몰랐어요로 넘어가게하지말고|1 + 그냥 뒤져 빙신아|1 +아 몰랑|0 +훔형 쿨타임 돌았구나인버스에 미쳐서 훔형 잊어먹고 살았어미안해 훔형..|0 +ㅅㅂ유투버새기들 꼴보기싫다유투브 언제망하냐|1 +저런 분을 뽑아준 지역구는 도대체 상식이 있는겨?|0 +아 존나 웃기라..|1 +시스템 메세지-|0 +관상이 바꼈는데|0 +돈 더 많으면 남 망하길 바라는게 정당화된다고 생각하면서 이런 댓글 남긴거노 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ |0 +이솜 하위호환 ㅈㅎ|0 +어무이 덕질 보소 ㅋㅋㅋㅋㅋ|0 +가세연에서 퍼온거 보솤ㅋㅋㅋㅋㅋ가세연 똥은 알지??가세연도 아줌마 부대가 점령 했더라김세의가 주예지 팬이라니까 아줌마들 부들부들|1 +아무쪼록 원만한 해결 되시길!|0 +병시나 워크넷은 실업급여받으려고 구라 지원도 많다 구지 통계자료 가져오려면 사람인꺼 쓰면됨 근데 내가 조사해보니 꿀알바는 증발한건 맞음 |1 +저거 백퍼 늙어처먹은 좌파틀딱새끼가 승인하거임.밑에 일러스터 젊은새끼는 장난삼아 나라를 사랑하는 애국에서 애자에 아이디어 얻어애몽? ㅇㅈㄹ하는게 아니고 걍 밥먹고 편의점에서 초코에몽 사처먹다가시발 이딴 우유에도 도라에몽있는데... 저작권 쯤이야하고 만들었는거임.위대가리 새끼들은 애몽에 애자가 애국이랑 비슷하니밀어붙여하고 생각없이 사인한것임.이거아니면 문재인이 개새끼|1 +먹버전용인듯 이혼남도 한번냠냠 전현무도 어린년만나기전에 한번냠냠 쟤는 저래놓고 똥차보내고 벤츠온다고 믿고있겠지|1 +대한민국 만세!!!|0 +미친색기 현대빨고 자빠졋노?|1 +호주 갈수있을진 모르지만 용접공으로썬|0 +유부우동 안먹었노|0 +싸우면 백퍼 지겠다 지렁이한테도 고전끝에 겨우 이기는데|0 +한심한 놈들 @@|1 +강남은 무료임 ㅎㅎ|0 +저새끼 다리 존나 짧네|1 +쪽바리 만화 보러 가느라 애썼~나베|1 +허준도 원래 장사나 하던 보따리장사꾼이었는데 뒤늦게 정신차리고 공부해서 의사 됐잖아 이거 보면 지금이나 별다를게없네 |0 +추천드려요. ㅎㅎ|0 +모하비가 훨씬 튼튼함 펠리는 걸레임|1 +샤부샤부 어디서 유래했노?|0 +무슨냄새남?|0 +니들끼리 꼭해라|0 +애미고양이 반병신 만들어 놓을테니|1 +진짜있는거임?|0 +우리나라 저능아들은 주적이 미국이라든데 ㅋㅋㅋㅋㅋㅋ|1 +남친 스트레스 많이 쌓일듯|0 +안그래도 요즘 숙박업도 울상일텐데......33오오 모이지말고. 시키들아.|1 +시즌 시작전 노리치도 우승확률은 있다|0 +나중에 바뀔 것을 생각하고 공동명의 주장하는거라고 게이야페미들이 더 나갈 것을 계산하고 있는거야요즘이 아니라 미래를 보고 움직이는거라고그리고 대한민국 하는 꼬라지로 보아 실제로 그렇게 될 가능성도 높음|1 +ㄹㅇ 점심때 불고기먹고 트림하면 후욱올라옴.|0 +영양제에서 문제는 캡슐이나 알약을 만들기위한 첨가제들이고|0 +난 왜 사진에만ㅠㅠ|0 +내가 틀니 극혐해서 틀니 놀리다가 30일 차단도 당해봤지만 넌 ㅁㅈㅎ 준다노친네 같은 컴맹 틀니 상대로 조립컴 추천하는거 아니다|1 +고딩들 가르치는 것 뿐만 아니라 성인 교육 시장도 마찬가지야 머리 굵은 새끼들 상대로 하다 보니 아주 대놓고는 표현 안할뿐|1 +인성이 바르면 '직업에 귀천은 있다' 이딴생각 안가진다.용접공을 봐도 아 열심히 용접하는 사람이구나중국집배달부를 봐도 아 열심히 배달하는구나생각하지...용접공 없으면 용접된 철물을 가질수없고중국집배달부 없으면 짜장면도 배달시켜 먹질못하니.|0 +존나 웃기네 이거ㅋㅋㅋㅋㅋㅋㅋ|1 +븅신새끼 어디서 존나 그지 같은 김밥패딩만 입고 댕겼나..두번 접으면 니가 입고댕기는 점퍼정도 부피도 안나와 병신아..식당가서 반접어서 의자뒤에 걸면되 아니면 빈자리에 둘둘말아 놓으면되고..요즘 가격좀 나가는 패딩은 경량화 되고 충전재 소재도 좋아..뭔 패딩이 다 풍선처럼 빵빵해서 부피가 다 큰줄 아냐?그리고 겨울에 따뜻한게 젤편한거지 뭐가 편한거냐?누가 너한테 롱패딩 안입으면 죽인다고 협박이라도 하냐? 세상불편해서 어떻게 사냐?|1 +순복음교회 조용기|0 +뭐하는 인간이고?배우? 개그맨? |0 +그러니까 사기도박꾼들 임...짜고치는 대국민 사기고스톱.|0 +생긴 거나 목소리나 딱 트렌스젠더 같은 게 더럽게 나대네|1 + 원래 사회주의 이론 자체가 존나 아름답거든|1 +사람들 착각하는데 우리나라 페미 이제초기야 아직 갈길이 존나멀었어|1 +가끔 저래 빡칠때 있지. |0 +라멘 어디서 유래했노?|0 +소총좀 사놔....|0 +원래 이상했는데 니가 눈을 감았겠지 요새는 무슨 |0 +공익에 반하는 사실은 명예훼손 성립자체가 |0 +이게 딱 일베 스텐스지 ㅇㅂ|0 +근데 공무원도 케바케라 빡세고 힘든데는 힘들다고 하더라아는 동생도 자주울던데 민원 진상들 너무 힘들다고|0 +송파랑 성동도 포함해줘|0 +문재인보다 문재인 빠는 시발 개새끼들이 더 좃같네 개간나 시발년들 |1 +응 강남 개포동에 썩다리 아파트가 20억이야|1 +아니나 다를까 가능 댓글 있네 시발ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +미안하다게이야 심적으로는 내가 너보다 수십배는 힘들다 진심이다게이야|1 + 그래서 계속 빡센 곳은 빡세고 편한곳은 편하니 정부의 잘못이지|0 +ㅋㅋㅋ 생각엄는 차주네요|0 +모노노케 히메|0 +내가 알기론 장면 하고 전두환 김대중 다 천주교 였던적 있었던거로 아는데|0 +아마존지금사면 늦음?|0 +개병신세끼!|1 +pornhub에 코스프레하고 떡치는거 있다 |1 +더 크게 골아라|0 +오히려 반대아니냐? |0 +황교안이 짤라버렸네 ㅋㅋㅋ|0 + 7등급이면 공부할 머리도 의지도 없는 좀비들이야.|1 +코로나를 이긴 만우절 ㅋㅋ|0 +=> 난 그런 말 한 적 없는데? ㅋㅋㅋㅋㅋㅋ왜 거짓말하노? ㅋㅋㅋㅋㅋㅋ|0 +근데 저기 3개월이라고 어딧음?|0 +고마워요~ 이런 꿀팁을 !!.|0 +저거 황해 하정우룩 아니냐?|0 + 나도 리재명 존나 싫어하지만, 이국종이 옳은 일 한거였다고 생각한다.|1 +근데 조쉬는 한국문화를 타 외국인에 비해 잘아니까 인기많은건데 그걸 질투하는건 ㄹㅇ 자적자고...|0 +예상 ㅈㅎ 주려다 문 욕하는거라 아무것도 안줌|0 +기생충들|1 +저런식의 여론조성은 결국 남자들피해로 돌아옴 나중에 가봐라 진짜후회한다 여가부시발년들은 철지난 성교육으로 ㅈㄴ남자들은 이상성욕자니 니들이 성욕을 해소하는모든방법은 다 불법적이고음성적이다라고하는 마당에 뭐가 해결되겟냐ㅋㅋ 야동사이트도 다 막는 나라다 생각보다 더 병신같은생각을 윗대가리들은 하고있음|1 +삼성꺼 같은데?|0 +ㅋㅋ병신새끼 다리로 떨어질거면 얼굴이라도 쳐박지 말던가 아니면 첨부터 그냥 머리로 쳐박던가 애매하게 떨어져서 둘 다 꼬라박네ㅋㅋㅋ다리부러져서 대가리 한 대 빡 때리고가면 쫓아오지도 못하겠노 ㅋㅋㅋㅋ|1 +억지같긴 뭐가 억지야... 그 수많은 사람들이 억지로 좋아한단 말이냐? ㅋㅋㅋㅋ 말이되는 소리를 해야지|0 + 북한은 존나 민감한 부분이라서 정부에서 돈 줄 끊어버리면 지들 잇속은 못챙기니...|1 +찾으러 갈때...|0 +최승호 병신|1 + 경찰이 거부하면 아무것도 못함 민간인만새됨|0 +씨발년이라고 할것까진 업잔아|0 +씨발것들!!!|1 +자식은 무슨 죄냐|0 +놀토랑 맛녀석들 재방 존나 하던데 돈 쓸어담겠네 ㅋㅋ|1 +멍청한 놈들|1 +하여튼 전라도놈들은 뒷배가 좋아 ㅉㅉ |1 +재미도없고 에휴 ..|1 +대구대구대구 진짜싫다~~~~~|0 +박근혜 애미 애비가 창자까지 돌림빵쳐당한 육변기 개갈보지 씹창년새끼 ㅇㅇ|1 + 진정성이 담긴 홍익강연들|0 +주작하네 시발 니가 삼전 무선시업부라고? ㅋㅋㅋ|1 +이새끼 위장홍어다 ㅁㅈㅎ줘라|1 +아 내 감수성 돌리됴ㅠㅠ|0 +나라서 웃지 못하겠네 ㅠㅠ|0 +얼추 맞구마ㅋㅋㅋㅋㅋㅋ|0 +팩트는 보니하니 20살 차이나는 동생을 주먹으로 패는게 어린이방송에서 그대로 송출사나 그냥 쿵쾅이들이 트집잡으려 했는데 실패 그냥 넘어감bj는 98%가 쓰레기새끼들만 있어서 욕처먹어도 마땅 아무 상관 없음|1 +이거 ㄹㅇ|0 +못따면? ㅁㅌㅊ?|0 +지디의 수많은 팬들을 감당할 수 있겠냐?|0 +강제로 끌려간게 소수 일거다. 위안부 절대 다수는 일본여자들이었고|0 +gdi엔진이 엔진오일 잡아먹고수리비 들어갈 일 많아져|0 +둘다못쓴다|0 +BBW 취향이긴한데 가슴은 빵빵해서 좋다 이기|1 +예전에 강서구는 서울이라고 말하기도 부끄러운 동네였다 |0 +머구는 가능할걸|0 +베트남도 똥남아에서 한가닥 하는 나라 아니냐? 미국,프랑스,짱깨도 몰어낸 나라잖노?|1 +니가 근거 가져오면 니가 이기는 게임 아니겠노 이 씨발년아 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +수많은 시체팔이로 단련된 +9강멘탈 소시오패스 종결자 ㄹㅇ|1 +OKDAE 애를 키워봤어야지 알지|0 +조 중 동 |0 +ㅌㅋㅋㅋㅋㅋㅋ 보배아재들이 신천지 재산좀 불려주게생겼네요|0 +오늘도 여자에 미친 일게이들 ㅁㅈㅎ|1 +미국이 이란산 원유를 중국으로 수출하는것을 제제하는 핵심은 뭐냐? 그래서 국제시장질서 , 이란도발을 방어하기위한 목적의제제 , 중동정세의 안정 , 호르무즈해협의 항구적 상시적 선박항행목적의 평화 유지이란의 깨어있는 시민의식만들기|0 +윤석열 청원 답변 예상 ) 검찰개혁 국민의 엄중한 명령 최근 인사 위법이 아닌 지극히 정상적인 정기적 인사의 하나 청와대는 아무 잘못없음 응 조까 검찰개혁 완수할꺼야|1 +틀베라케|1 +16살때 질싸당했네 ㅋㅋㅋㅋ|1 +ㅆㅅㅌㅊ|0 +젤 공장충들 화장실에서 오줌 모아놨다가 젤에 넣음|1 +알로하 항공은 섬들을 연결하는 초단거리 위주 항공사라 고도가 낮앗음 약 2만4천피트 였음근데 저것도 운좋은거임 , 만약 금방 차륙못햇으면 저체온증에 쇼크사로 죽었을거야애초에 제트여객기는 속도가 빨라서 저정도로 외피가 까지면 바로 분해되는데 운좋았음|0 +그정도 되면 뷔페가 편의점 삼각김밥먹는거나 마찬가지일듯|0 +맞다 기억해줘서고맙다|0 +노무현 오랜만에 외출 나왔노|1 + 오히려 무시해주면 좋아함 열악한척 하면서 꿀통에 꿀채우거든|0 +ㅈㄹ 가난이 죄냐? 기다려주는 것도 필요하다. 세상사는게 그렇다. 어린새끼라그런지 좆도모르는거같은데 너처럼살면 칼맞어 |1 +얼마남지 않았다 ㅋㅋㅋㅋㅋ|0 +주님의 경륜이 아니면 불가능한 표적 이자나|0 +비수기라하더라도 12월에 순수익100조금넘게벌림ㅜ 보통 300-400은벌리는데ㅜ 사람들이 돈을 안쓴다ㅜ |0 +아임뚜렛도 울고갈 연기력 ㅇㅂㅇㅂ|0 +와 !|0 +사탄: 너를 나의 후계자로 지명했다|0 +저거 무슨 상황이냐 ㅋㅋ|0 +자막도 마이너감성 ㅋㅋㅋㅋㅋ|0 + 사회주의성향에 범위선정 제대로 못하고 떠드는걸|0 +조까 ㅂㅅ아 살면서 만날일도 별로없는 성씨들가지고 한무당질하지마라|1 +미친놈 어디서 되도 않는 주작질이노|1 +바랄걸 바래라. 발음이라도 제대로 했으면.|0 +진지하게 현역 서성한 밑이면 걍 포기해 허송세월가능성 높음 나이먹으면 대가리굳어서 더힘들다|1 +극단적 선택이 자살만 있는 것더 아니고 개나소나 극단적 선택이래 ㅋㅋ|1 +그분들께는 음식점 영업하지마라는것과 같은 논리입니다 ㅋㅋㅋㅋㅋ|0 +돈사랑교회|0 +보수유투버들 대부분이 저런 식이야노력해라 노력노력안한 니들 잘못허구헌날 노력타령황+장수 등 사회시스템과 정치인의 문제로 보는 극히 일부 보수 유투버를 빼고는 다 그런 식임|0 +돈버는 재주는 좋네|0 +이사떡ㅋ|0 +ㄴㄴ 할아버지는 지장보살멘탈 호인이고 아줌마는 츤데레임|0 +명문가 양반집 자손이면 이해라도 가는데 지금 대한민국 국민 99%는 그냥 쌍놈들이다. 쌍놈들이 뭔 제사냐? 지들이 양반집이라는놈들 족보는 다들 있고? 족보 있으면 대대손손 내려오는 문집들은 있고? 이건 정말로 위조가 불가능하거든 하루이틀만에 만들수도 없고 지식이 없으면 만드는것 자체가 불가능한게 문집이다. 문집없는것들 다 쌍놈들이다. 그리고 선조로부터 내려오는 선산들은 다 있냐? 이런거 하나라도 없는것들은 다 쌍놈들이다.|1 +경비수료증 있는사람ㅁ2ㆍㄴ됨ㅅㄱ|0 +그냥 계속 금지해 뭔 2개월 ㅎ|0 +다음 가봐라... 진짜 거기는 종교더라 ㅅㅂ 바로 탈퇴했다 카카오도 곧 탈퇴할거다 |1 +노트북등 IT 제품은 브랜드도 중국산이아 등신아 꼭 좆도 모르는 새끼들이 컴퓨터 아는척 오진다니깐 비유도 등신 같이함 16기가 브랜드 란다 씨발 ㅋㅋ괜히 모르면서 나서는 애들 보면 김치년 보는거 같아서 딥빡이 쳐오름|1 +주예지가 진정성있는 사과를 하려면 이런 애랑 결혼해줘야 한다.|0 +이분도 가실때가...|0 +좃 때라 쫄보새퀴야|1 +병신짓엔 한마디할수도 잇지|1 +명도가 머임?|0 +대한민국이 일어서다니..정말 기적입니다.|0 +이제 기름도 퍼부어야지?ㅋㅋㅋ|0 +에휴|0 +일베새끼들 하고 싶은말 많으면서 아끼는거 보소 ㅋㅋㅋ|1 +그건 ㅇㅈ ㅋㅋㅋㅋㅋ 일단 채우자 꿀잼각 ㅋㅋ|0 +난 월세 50냄|0 +도장런이 뭐냐|0 +강도질을 하지 않았어도 횡령을 하면 유죄가 된다. 아무리 많은 착한일을 하더라도 하나의 죄를 지으면 유죄다.|0 +목사한테 빤쓰내림!|1 +이젠 구더기도 불쌍하게 느껴짐|0 +내가 이래서 결혼하기가 무섭다 결혼하기전에 어떤년인지 어떻게 알 수 있겠냐 막상 그 본성이 마지막에 나올지|1 +신발보다 싸|0 +예쁘긴 한데?|0 +쌀한가마니 정도 무게 자주 들어야되냐??|0 +!!!!!!!!|0 +아이나꼬 딸라가 말라끼 띠띠 마간다나 아이나꼬 딸라가 아농기낭 가와모 아떼 마사낏 챤 왈라나 빼라 모 |0 +응 저것도 다 대본 ㅋㅋㅋㅋㅋ|0 +12월에 생산된 전시차량입니다.|0 +둘다 병신새끼들인데 잘만났구만|1 +ㅋㅋㅋㅋ ㅂㄷㅂㄷ 거리는거 보니 대대로 그지새낀가보네 .|1 +저 여자 욕하면 나 고소먹겠지?|0 +쓰레기|0 +저기요, 지갑에 2천만원짜리 수표가 없네요...님 고소!!! 이런 스토리로 부탁한다. 4월 1일에 보자.|0 + 같은 급수인데 왜 업무량에서는 차이가 나는 거임?|0 +사람색끼 말을 믿읍시다|1 +씨발 존나 무섭네유관순 열사님 ㅠㅠ|1 +백화점 vip는 안 빼줌....롤렉스 자체 vip되면 스틸모델 입고 시 먼저 연락준다는 썰은 들어봤는데 롤 vip 되려면 금통을 많이 사야해서...|0 +아니 시발 수사권에 기소권까지 가지고 있으면서 법원까지 점령 당했고, 검사가 수사하려고 하면 공수처가 중간에서 가로채는데|0 +결국은 조직의 최상위 권력자들 따까리 밖엔 안됨.|1 + 공산주의에 대해서 배우고 느낀 거라고 하더라|0 +존나 웃기지???더 화를 불러내는 행동을 해 그러면서 차별을 하지 말래 미친 개새끼들이 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ깔끔하게 단정하게 매너있게 시위 퍼레이드해도 시선이 바뀔까 말까인데 좆같이 벗고 어린애들 절대 보면 안 될 미친짓을 한다구... 거 참 이해 못 할 새끼들이야 그러니 항문 똥나오는 곳에 쥬지를 넣지|1 +왼쪽은 할아버지 아님?|0 +이런 고소 고발이 추후 신천지를 탈탈 털 명분이 되어줄겁니다. ㅋㅋㅋㅋㅋ|0 +돈 없고 외국어 못하면걍 저세상으로 이민가라당장 갈 수 있음|0 +역시 일베구더기 모자리들은|1 +지자체 사람들은 그 동네 살겠지만, 그래도 그 사람들 그 지역에서 중심이 되는 시 단위에서 산다. 거기서 살면서 자기 근무지로 출퇴근하는거지. |0 +심적 여유가 그나마 있는 거임.|0 +주예지 생각이 대한민국 여자들 생각일까 두렵다 뒤지게패고싶은 밤이다|1 +그래 니말대로 그렇게 나누려면 그리 해라|0 +그거슨 주인님과 노예의 차이|0 +있어 그거|0 +유치하다..|0 +이건 나도 어느정도 동의함|0 +난 말죽거리 잔혹사 본 이후로 집에 혼자 있을 때 거울 보고 대사 연습함야 니가 그렇게 싸움을 잘해? 옥상으로 올라와이거 몇천 번은 해봤을듯|0 +공부하다가 자살하라는 것과 마찬가지임|0 +충선왕이 소를 좋아했어!|0 +발기부전 냉동자지 에 |1 +빠른 테크트리도 좋은 옵션인디 흠|0 +니애미 김정은 후장빨이년 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +저말이 틀렸냐고 대깨문들아 팩트로맞으니 아프니|1 +아찔한 조언이네|0 +가세연에도 나오던데 좌빨 강사면 절대 안 나가지|1 +갱북도 홍어잠식 많이됐음|1 +기능고장! 두부고장!|0 +수능은 94년 실시 이후 항상 백분위와 등급을 같이 표기했습니다.|0 + 돈많은 도련님. 잘 들으세요. 세상에는 다 그에 맞는 물건이 있어요|0 +곧 4월이노ㅠㅠ|0 +우리의 인격은 전범기 외에 기본 나라의 상징은 공격하지 않습니다. 삭제해 주세요. |0 +예전 전기일로 남부산림청 가봤는데ㄹㅇ 개꿀 빠는거 부럽더라 ㅜㅜ|0 +어이가없어서 웃음하고 욕밖에안나오네|0 +하여튼 뼛속부터 딴나라당~|0 +니말대로 천한직업이야말로|1 +최고의 지도자?|0 +여기 써있는 그대로의 문장이면 성실이고 자시고 니가 넘겨짚고 있는 것 같은데?공부할 생각 없으면 용접하란 소리 아님? 용접에 공부 필요없는거 맞잖아글쎄 뭐 호주니 뉴욕이니 하는 데서 이런 기능공이 돈을 많이 받는다고 하는데 실제 인식이 어떤진 모르겠지만기능공이 공부해서 얻는 직업이랑 같은 대우를 받는 나라였으면 이게 문제가 되는 발언일까? 그냥 공부 안 맞으면 용접해라 하는 다른 선택지를 제시해준 것 뿐이잖아이게 기능공 비하발언이라고 주장하는 사람은 그 사람부터 은연중에 기능공을 한 수 아래로 보고 있다는 소리밖에 안 되는 것 같음|0 +알겠다이기ㅋㅋ 딱 채찍쳐맞는거 좋아하는 한국식 마인드네. 노예마인드. 조금만 성공한 사람이 뭐라하면 똥오줌 못가리고 찬양하기 바쁘노. 씨발 유명해지면 똥을싸도 박수를 쳐준다던데 정말이구나...ㅎ 손주은 영상 다시 한번 더 보길바란다. ㅂㅅ같은 개똥철학이나 읊어대면서 어린십대들한테 창녀운운하는게 좋게보이는지.. 그리고 그영상보면 학생 뺨싸대기 때리는것도 나옴. 편집해서 영상은 안나오고 소리만 나오는부분.. 그런것들은 현실능욕이라고 단순히 치부할 것들이 아님. 인간 쓰레기들이나 할법한 말이고 행동이지. 손돼지새끼 인성은 예전부터 유명했음. 어린애들 상대로도 감정조절 못해서 싸대기때리는건 예사고, 잠깐 꿈뻑 조는 학생한테 "넌 삼수나 해, 이새끼야!" 라는 식의 폭언도 일삼았었음. 강사는 수업 가르치는거나 잘가르치면 될것이지, 언어적으로든 신체적으로 폭력을 일삼는다면 그건 인간쓰레기지. 미국이었으면 깜빵갔을일이다.|1 +어제 야간작업까지하고 술쳐먹고 곯아떨어진듯ㅋㅋㅋ|1 +알아서 여자애가 고백하든가 고백 비슷하게 멘트 날라올거임|0 +응 페미년 자국이라고 나누는 너가 인종차별주의자다|1 +인터넷, 유튜브의 발달이 양날의 검임 ㅋㅋ근데 그만큼 또 빨리 사그라들고윾튜브처럼 도리어 뻔뻔하게 행동하면 위기가 기회로 바뀜 ㅋㅋ참 한국사람들 다루기 어렵지|0 +명박놈 방송|0 +수영할때 음파음파|0 +정말 가지가지하네여|0 +이새끼 백퍼 보배분탕이네 ㅋㅋ 내가 말이 많으면 논리로 반박해 시발년아 어서 좆거렁뱅이새끼가 최저시급타령이여 |1 +돌고래도 팬 쥐어주면 7등급은 안나온다|0 +주어없음 안됨 안바꿔줘 돌아가|0 +그냥 병신 프로그램|1 +난 그렇게 생각하는데 그래서 대한민국 정부도 일본의 항공을 막는거 아닐까?|0 +아주먼 옛날에 지구까지 왔던 외계인 이라면 |0 +사장이 전라도인 구라미터를 믿냐?개표기 조작이 문제임|0 +이런분이...국회로 가셔야....하는건 좀 오바인가요??|0 + 같은 자국민이 아니니까 좆도 하루한끼 먹이고 하루종일 일시키고 개구리에 돌멩이 던지듯 쌍놈들 대가리에 돌던지고 여종들 돌림빵해도 양심의 가책을 받지않을수있는거임.|1 +아 그래서 용접해라?|0 + 머가리 용량 다차서 트져 뿟노?|1 +확실한건 술병은 걸림|0 +하여간 흉기 언플 존나함. 이제 안먹히지 자국민이 호구도 아니고|1 +병신새끼 지 이야기 쳐하고 자빠졌네.|1 +마키노 텐동집 갔나보네 난 교토에서 먹어서 교토인줄알았더니나라도 있었누 체인점인가보네 여기도 줄많이서던데 난 거기 2번가서메뉴 다르게하고 조개국물 추가해서 먹었는데 ㅆㅅㅌㅊ|0 +3. 인간의 구원에 육체는 중요치 않다. 그리스도인들은 율법에서 해방되었기 때문에 육체가 짓는 죄는 정죄의 대상에서 제외된다.|0 +그런말 하는 너도 결국 당해보면 그딴소리 하지도 못할걸?|1 + 벳남 현지인 친구가 붕어빵 사진을 보여주며 한국에서 이거 얼마냐고 물어봄|0 +2. 죄에 대한 형벌을 깨달을 것|0 +가만히 좀 있지 왜 얘기해서 통까흥 알고리즘 가동했네 씨발|1 +뒤를 쫓는 그림자는 명탐점 (명탐정) 바베크 (바베크)|0 +뭐 눈에는 뭐만 보인다고 전라도 까는 애들이 전라도스러울 수밖에 없음|0 +혐세의 한국외대 나온 성제준 엄청 무시하던데 (지는 삼수지만 서울대라서 이해는 함)같은 중경외시 주예지 겁나 까겠노ㅋㅋ|1 +ㄴㄷ 정규재 신의한수 안정권 좌좀빨갱이 홍어새끼|1 +정상이 어딨노 다 본성은 미친 마음이지. 근데 깨달은게 본성을 거스르고 반대로 나아가는게 인생인거 같더라. 본성대로 살면 좃같아짐,, 특히 같이 살면.., 여튼 감사.|1 +이제 중국이나 일본 이미지가 영~~~|0 +지랄도 풍년일세~~|1 +내가 173에 저러고 다니는데 시발미안하다 이기..|1 +와 난 55인치인가 그거 이번에 사면서 와이프보고 큰티비를 모하러 사노 했거든 어차피 테블릿 많이 쓰니깐 75면 존나크것다 진짜ㅋㅋㅋㅋ|1 +휴게소 화장실에 스티커 붙이는건 야들 전문이거던|0 + 예상보다 빨리 독립하겠다. ㄹㅇ|0 +니가 지금하는 소리가 뭔소린 줄은 알고 하는거?|0 +고뱃은 진도 다 뺀후 하는게 정섯임적어도 키스는 하고 고백해 아다새끼들아|1 +일본 걸스채널 같은데서 전형적인 남자의 망상으로 만든 작품들이라며 까더라 ㅋㅋ|0 +그랬던 친구 어머니가...|0 +그러나, 오 허무한 사람아, 행위 없는 믿음이 죽은 것인 줄 네가 알고자 하느냐?|0 +정확히는 독학재수학원이었음|0 +화성은 사람이 살 만한 동네가 아니다무적권 걸러라|0 +기술직 폄하자체가 병신아 차라리 공장간다고 하지빙신아|1 +시발년이네남친 잇냐?|1 +쉬크만쓰다가 질레트 스킨텍 신제품나왓다고 2500원에 2개 주길래 써봣는데 ㄹㅇ 신세계다보통면도할때 힘살짝줘서 미는데 이건 ㄹㅇ 그냥 미끄럼틀됨 그렇다고 안짤리는것도 아니고 두세번밀어주면 자극1도업고 잘밀림쉬크꺼 날 아직 5개남았는데 갈아탈지 고민이다 이기|0 +의대생인데 의대교수님들이 항상 강조하시는게 '인성'보다는 '실력'이다. 왜 그런지 아냐??아무리 사명감이 투철하고 배려심이 깊어도 실력 후달려서 매스질 잘못하고 오진 한 번 하면 사람 죽는거야. 우리나라 인간들이 얼마나 열등감에 찌들어있는지 알 수 있는게 그 분야에 종사하는 전문가들의 견해따윈 개무시하고 그저 지들한테 쉬운 도덕적 잣대로 남들을 흠집내고 까내리는게 요즘 대한민국 풍조임. 참 개탄스럽다. 아 맞다 문재인 씨발개새끼.|1 +레알 둘다 병신.. 근데 안물어보고 멋대로 해버리는 인간들은 상종안하는게 답이다.|1 +그래도 책임지네 남자답다|0 +저런사람들보면 내가죽더라도 진심 전쟁나길 기원함|0 +ㅜㅜ|0 +키라라같기도하고|0 +보고느낀것레미본야스키 사진씹간지최홍만 브록 경기존나기대햇엇는데 김민순가누구로 대체대서 노무아쉬었던기억홍만이 타이슨은언제만낫누|1 +저세끼들이 어떻게 무너지고 남은 인생과 주변이 끝장 나는지 꼭 지켜본다.시발|1 +일부로 알면서 뛰어내린거|0 +쪽본은 이대로 쭈우욱 좟되길|1 +안장 뜯어가야|0 +이게 진짜 벌이다|0 +신천지사람들은 치료안해준다고 발표하면되는데.. 개새끼들|1 +이만희가 지시했다면 이만희는 반드시 생전에 천벌을 받는다!|0 +개노답 대구|1 +저놈의 30프로|0 +비슷하게 생기게 된건 고려 이후 피섞여서 그리 된거지. 그 전은 완전히 달랐을듯|0 +415이후엔 당분간 안보이겠군|0 +사기당하는거 유튭채널 개설하면 돈 존나게 벌듯|1 +통까면 너 홍이지|0 +전화는 누가 받아?|0 +자기가 짠따다 싶으면 돈 많이 벌면 된다 나처럼|1 +깊은 빡침|0 +이래서 개독 개독 하는거군요|1 +외조카들 존나 이뻐해주는 삼촌도 있다 이기얌.|1 +유교성리학 사회문화가 아직까지 남아있어서 그래 이게 뭔지 알잖아?|0 +막지무지 존나귀엽다 나 맨날보는데|1 +ㅇㅈㅋㅋ 속으로는 ㅈㄴ게 찔리는일게이들인데|1 +저사람 개인적으로 안다 복싱챔프는 맞음 근데 단체가 첨들어보는 단체였고 전적도 4전4승인가 6전6승인가 얼마안됨|0 +개독 일베 핑크탕 신천지가 싫어할 내용이네여|1 +네례한국당은?|0 +기술직이 아니라 기능직이야 기술직은 4년제 엔지니어들이고|0 + 내가 한 착한일이 나의 죄악을 덮어주지 못함.|0 +게이야 나랑 상황이 비슷하노 나도 개 좇같은놈 땜에 미치겠다 어쩌겠노 인생 길게 보면 이 순간도 곧 지나가겠지하고 맘 잡으면 또 씨발 좇같은새끼 죽이고 싶고 매일 반복이다 근데 별것도 아닌 병신땜에 내 중심이 흐트러지면 쪽팔리자나. 힘내자 소중한 가족이 한새끼땜에 분위기가 안좋으면 되겠노 항상 니가 젤 위다.|1 + 한때 한솥밥 먹던 진중권이 저 진영에서 잠시 이탈해 좌파새끼들 개소리하는거 저격하고 있으니까 저렇게 주목 받는거지 우파라고 무슨 아가리 닫고 있는 그런 상황 절대 아님|1 +[이게 쓰고 안쓰고 했다는 증거이면서도 안일한 대처라고 취부하면 메뉴얼 삭제는 아니다 하기 어려우니 애매한..]|0 +신림 갑을 아파트 5억|0 +석유 화학은 GS, S-오일, 현대오일뱅크지|0 +야구랑 농구에 비해서는 많이 못범|0 +위에 왼쪽사진봐라.|0 +안동에 왜 가셨니껴?집이 안동이니껴?식사는 맛있게 했니껴안동에 마사지는 어떠니껴아 ㅅㅂ 한우 보고 한우먹으로 나가야겠다|1 +ㄱㅅㄱㅅ 덕분에 속이 시원해짐|0 +저렇게 돈많은 승리놈도 못없애고 있는데 뭔 방법을 찾냐|0 +중국간첩|0 +얼굴도 ㅍㅅㅌㅊ인데왜 저런짓을할까|0 +마. 쫌 아노|0 +약 ㅇㅇ 문페이스 보이노? ㅋ 내장비대증에|0 + 김밥은 어느 나라 음식이노? ㅋㅋㅋㅋ 이 씨발년아 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +전시라고 말했고 여럿이서 사람 병신만들기 쉽고 죽은자는 말이없다.내목숨을 위해서 기꺼히 엿같은 새끼에게 내 K2 소총으로 후장을 갈겨줄테다 그게 간부라고 해도 말이지간부새끼땜에 죽나 다르게죽나 죽는건 매한가지|1 + 내가 얼굴만 잘생겼으면!|0 +댓글 이미지 올리기|0 +14년 유입 주제에 나대서 ㅁㅈㅎ|0 +80대?|0 +백퍼저거 다시 다주워먹음|0 +2번째 굴뚝은 왜 뭉개져있죠|0 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +큐브스테이크덮밥 시켜도 저거보다 고기 많겠다|0 +그네:어서와 깜방은 처음이지?|0 +피복아크용접 이라는건데 용접시 고열로 철재가 금방 부식되기 때문에 산화를 방지하기 위해 이산화탄소를 끼얹어 공기를 차단하면서 용접함용접봉은 피복에 쌓여 돌돌 말려있는데 용접건 스위치 누르면 이산화탄소와 함께 찌~익 줄줄 나옴힘이 좋다고 해야하나? 모재를 파먹으면서 쇳물을 채우는 특성때문에 인장강도가 강해야하는 용접에 적합하고 용접건만 다룰줄 알면 되기때문에전기(아크)용접이나 알곤 용접에 비해 장시간 용접작업시 피로감이 덜하고 빠른작업이 가능해서 조선소나 여러 공장에서 두루 쓰임단점은 불똥이 오지게 튀기때문에 전신을 보호할 보호복을 입고 작업해야 한다는점이 최대 단점ㅋ|0 +박나래 아니냐?|0 +일본은 금융으로 돈 버는 나라다|0 +양|0 +조까셈 내 친구도 지능 솔직히 명수보다 ㅆㅎㅌㅊ데 돈 잘벌고 여자 잘사먹고 다님|1 + 신승범,최진기 은퇴시키고|0 +그런걸 실시간으로 느껴야 먹고사는거지 |0 +본능적으로 싫은건 어쩔수 없으니까.|0 +정치 종속자들|0 + 그렇게 생명이 소중하면 덜 소중한 돈을 쳐 가져와놓고 그딴소리를 씨부리던지 ㅋㅋㅋ|1 +물론 논란의 여지가 있긴 하지만 이렇게까지 사과영상 올리고 마녀사냥 할 내용은 아니라고 생각하는데... 좀 과열된듯|0 +ㅆ선비들 많네. 저게 못할말도 아니고, 7등급이면 용접해야지..|1 +왜 여자들이 경찰에 신고 안할까? 라는 생각들만큼|0 +함소원아 대치 은마상가 중고명품샵 기억하지? ㅋ|0 + 1년 이면 마누라랑 별로 안하고 싶음 ;;|0 +뒤져라|1 +보내봐라|0 +음 근데 일게이들은 관계없자나. 결혼 안할거자나?|1 + 어휴 ㅋㅋㅋ|0 + 나도 그래서 약먹는중|0 + 관공서 부터 하다못해 인터넷 휴대폰 개통 등등 모든 문제를 본인이 해결하고 법적인 문제등등|0 +말 논리적으로 잘하네. 의외로 천재일지도|0 +비정상이지. 저거 무조건 파혼해야한다.|0 +2주면 인당 5점씩 이고 3주 이상이면 15점이니|0 + 이게 민간기업들끼리 정상적인 경쟁이 이루어지는 시장에서 가능한 이야기냐? 불가능이지.|0 +먹는건 어쩔수 없음 자연의 섭리니까|0 +저정도 말투로 이야기함 사실 사석에서 술자리면 싸움날수도있음|0 +닉 커엽누|0 +나쁜짓 뭐했는데?|0 +그깟 토션빔 달고 HD승차감도 못쫒아감|0 + 민주당 드루킹댓글조작알바 새끼들 정신 못차렸네~~~~!!!|1 +https://www.bobaedream.co.kr/view?code=accident&No=624096 |0 +노페단독에서 노페코오롱 네파 아이더 k2 디스커버리 등등 등산 대장급패딩으로간다음 롱패딩 간다음 숏패딩 갔지|0 +뭔 이어폰으로 돈을 저렇게나 많이 버냐? ㄷㄷ...|0 +그냥 길거리에서 돌로 때려죽여야...|1 +예전에 이분도 넋놓고 있다가 후친 차량에 치였죠. ㅋㅋ|0 +주인 어딘가에 조난 당했다 개를 따라가라|0 + 이 새끼가 이 글에 나온 틀딱마인드네 딱 ㅋ|1 +씨발새끼들......|1 +횡단보도가 있는것만 클릭하세요.|0 +저도 대출을...|0 +거기색깔은 석탄이랑 비슷할듯|0 +짱깨유학생한테 들었는데 지들도 지네나라 식품 못믿어서 저러는거라카더라. 비닐포장되어있는것도 다 찢어서 만져보고 산다고함 ㅋㅋㅋㅋㅋㅋ|1 +애초에 똥이 담겨있던 내장을 쳐먹는데누린내가 안날수가 잇냐 씹병신년아대충 쳐먹든가 아니면 락스로 빨아 쳐먹든가 병신년아ㅁㅈㅎ 놓고간다 리뷰충새끼한테 전해주라|1 +왜 까면 안됨? 지금 조선시대임?|1 + 지금 이란이 더 잘산다? ㅋㅋㅋ |0 +해머가 존나쎄고 나머진 쓰레기|1 +제너럴 조승희|0 + 웃고간다 병신아|1 +보빨 ㅇㅂ|1 +필리피노 ㅇㅂ|0 +저 년만 그런게 아니라, 일게이들도 평상시엔 저럼요.|1 +님이 말씀하시는 목사아들은 '나꼼수의 돼지엄마 김용민PD' 잖아요|0 +포인트 회수 처리함|0 +예를들어 악으로 가득차도 순수하긴 하죠.|0 +걱정마 넌 암으로 안죽고 굶어 죽을거여|1 +삽자루가 뭔 우파여 또라이인가|1 +솔찍히 |0 +[삭제된 댓글입니다.]|0 +핵융합로 완성! 발표를 못하는 이유>> 세계석유시장때문에... 미국주가곤두박질 중동은 거지나라로전락하고러시아는 다시 3개국으로쪼개짐 짱깨는 기름값요동 처서 개망함 일본은 지네가쓸 기름만 뽑아냄 한마디로핵융합로완성은 경제를 요동 치게만들고 새로운 경제구조를 만들게된다 그게 이반도놈들이이룬 성과다 헤헤,,|1 +똥꿈은 돈꿈이 아니지|0 +그냥 가만히있어도 빠지는거야???ㅋㅋ|0 +와드|0 +재산 한 50억 잇을라나|0 +탕안된게 어디냐|0 +말 좆같이하는것보소 씨발련이 도끼로 대가리 두동강 내버릴까보다 십새꺄|1 +한국넘들 이중성이 머냐면, 지가 안당해보면 아주 옳은 소리 하는데, 지가 당하면 다른소리한다.ㅋ|1 +지방도 번화가는 잘되더라. 번화가 아닌곳만 뒤지는거지 ㅋㅋ|0 +흑수저로 태어 나면 부모는 자식에게 아무런것도 바라면 안되는게 맞음존나 못살게 태어나서 너는 성공해야 된다라는 말을 하는 부모 있으면그냥 호적파서 나와라니 인생은 가족에게 평생 호구가 될것임|1 +비꼬는 투로 들리는 거|0 +갸꿀|0 +우리느나라 본선 나가냐?|0 +뒤에서 박는것 보다 차선 변경 사고가 안전빵이지|0 +그럼 꼽아 놓고 하면 좆질로 분수 가능?|1 +어제 만은 혼자 있었다|0 +눈뜨고 보기 민망해서 스크롤 내리고 들었다 ㅋㅋㅋㅋ|0 +그만 부들거려 노가다새끼들아|1 +지랄 옆차기 중..|1 +ㄹㅇ 글로벌리스트들의 수법이다|0 +1억2천|0 +나처럼 세컨폰으로 24시 녹음하고다니면됨|0 +뭔거짓말븅신아|1 +경위 이상 사법경찰관만 가능..순경부터 경사까지는 경찰관리임..|0 +이제 눈치 챘구나.|0 +부모님도 노무딱 좋아하시고..|0 +렉카 모는 애들이 얼마나 된다고...|0 +내 숏패딩은 캐구 칠리왁 사람들 은근 별로 없음. 다른 캐구는 존나 많은데 |1 +와 씨발 상상불허|1 +이건 문재인이 잘했네|0 +쟤는 레알 ㅈ됨ㅋㅋ요즘같이 성관련 처벌 심하게하는때에죄질이 안좋아도 너무 안좋음제발 몇십년 감방에서 썩었음좋겠다|1 +캌ㅋㅋㅋㅋㅋ콬ㅋㅋㅋㅋ쿸ㅋㅋ|0 + 무슨 4차원 포털 열고 따로 들어가야 하는 동네도 아니고 |0 +개소리 머엉|1 +애국보수 페미니스트 진중권 대통령 가즈아!!!!!!!!!!!!총리는 김무성 법무부장관 나경원경제부총리 유승민 행안부장관 장제원문체부장관 김성태 국토부장관 권성동|0 +축하해|0 +8번 항목에 써놓음|0 +저때는 개인회생 제도가 없었음.|0 +그게 왜그런줄 아냐 지금 그렇게 하다가는 좃되거등|1 +시골의 면사무소가면 다 그렇다 다 놀고있다 ㅋㅋㅋㅋㅋㅋ한쪽벽면에는 출산장려금 포스터나붙어있고 휑함 |0 +따악 화산이었으면 천베인데하필 따알이네|1 +물론 은혜의강 같은 멍청한집단은 욕먹어도 쌈. 욕 더먹어야됨|1 +돈받고 짜고치면서 개소리는..|1 +오연수 이연수 다른 사람인데|0 +인지과학을 공부하겠다는 말은"나는 자연과학을 공부하겠다, 인문학을 공부하겠다" 이런 말하고 똑같다.특정한 실체가 있는 고유한 학문적 특성이 존재하는 단일 학문이 아니라, 여러 학문들이 몇가지 공통사항과 패러다임을 두고서 다소 느슨하게 연결된 연합체 같은거다인지과학이라는 학문의 핵심 패러다임은 computationalism(계산주의)와 정보처리라는 것인데 이건 게이의 관심사와 많이 멀 수 있다.인지과학이라는 커뮤니티에는 다양한 학문들이 공동체를 이루고 있는데, 거기서부터 시작해야 한다. 심리학이면 심리학, 신경과학이면 신경과학...철학, 언어학 등등..근데 저 개별학문들 자체도 엄청나게 방대하다. 철학을 예로 가져오면 인식론이나 philosophy of mind(한국은 심리철학이라고 이걸 번역하는데 난 이게 너무 맘에 안든다)분석철학 등등 상당히 무게감 있고 어려운 분야들이 있고, 심리학은 그 자체가 인지과학의 하나의 핵심코어인데 심리학자체도 무지하게 방대하다..지각 감각부터 해서..신경계를 파고드는 분야에서 부터 인지심리학까지..그 하나가 또 여러 세부주제로 나뉘고...|0 +비타민이중요하냐 3대영양소가중요하지 탄수화물지방은 서로어느정도상호작용한다쳐도 필수아미노산은어쩔건데?|0 +병신 니가해보든가|1 + 서울대도 취직못하는 사람들 있으니|0 +윤서인은 결혼했으니까 연예에 환상이 없음|0 +맞는말이구만|0 +조센에 돈벌러 와서 조센 당하고 돌아간 외노들때문이지큰돈 벌면서 뒷다마 까는 똥남아 놈이나 노비마냥 돌리는 조선놈이나 둘다 똑같은 후진국 민도|1 +ㅇㅇ 짱깨 유튜버들 어이없는 상황 컨텐츠가 유행임 주작성이 심해서 대부분 티가남 샷시점이 어쩌다 찍은 스냅샷이 아님|1 +텐트안에 안추움?|0 + 역시 도로위든 어디든 기운 안젛은 새끼는 걸러야되는게 진리인거 같다 ..|1 +뭐가 레전든데 씹새야 ㅁㅈㅎ나 쳐먹어|1 +저게 사실이면 아이돌 80%는 사형이노...|0 +존만한색기|1 +이것은 모든 입을 막아 온 세상이 하나님 앞에서 유죄가 되게 하려 함이니라.|0 +질싸?|1 +그럼 뭐야.. 모녀사기단이 짜장을 캐스팅했고, 짜장은 이왕 이렇게 된 거 마지막까지 가보자~ 뭐 이런 스토리여?|0 +응 아잇!발시려 푸르르|0 +지랄 취업비자로 들어온 애들은 한국인이랑 임금 같음 ^^아빠회사 이제 외국인 안쓸꺼라고 존나 게으르다고참고로 토목회사임|1 +키즈노트로 뭐했는지는|0 + 가난한사람도많지 애들끼리 모여살다가 고독사나 굶어죽어서|0 +씹벌레색기가 머라는거야..? ㅋㅋㅋ|1 +아니 병신아내가 댓글 앞페이지에서 예지누나 이쁘냐고 물으니까 니가 니입으로 ㅈ같이 생겻다며? 왜 시비냐?그리고 좆같이 생긴 누나로 어떻게 딸잡냐?|1 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 부끄럽네 씹쌍규 저희 지역굽니다. 아주 개새끼죠 ㅋㅋㅋㅋ|1 +조가튼 소리하고 있네 자살시도한 사람은 80프로이상 다시 자살시도 하는 |1 +손기술이라고 하는게 옳지 머리랑 상관없음|0 +팩트라기보단 쓴소리에 가깝지쓴소리영상으로 아직도 회자되고 있으니까|0 +닉이랑 짤 존나 귀엽노|1 +16년 1월임용이면 20년 2월 자동진급이고 그동안 승진은 못햇나보네|0 + 그럼 뇌물현으로떰ㅅ냐?|1 +어휘에 흙뭍어있는것처럼 투박하네|0 +응 경찰대 떡상 ㅅㄱ|1 +10~20년후 법무장관 후보 ㅇㅂ|0 +이짤은 보면 볼수록 다각임ㅋㅋㅋㅋ|0 +씹세 안녕~~|1 +아침에 위치알려주는거만큼 짜증나는일이 없음.개병신새끼들 주소를 주고 근처건물위치 알려줘도 어디냐고 전화꼭한다|1 +주게이 크루수장 무시하노!!!|1 +아리가또 투~~|0 +조선인들이 과연 저리행동을할까|0 +아니 일게이새끼들 200명만 비추찍어도 버티는데|1 +님아 그거 앎?님 이름 석자 중국어로 읽으면 비슷하게 들림 ㅋㅋㅋㅋㅋㅋ|0 +중딩 같지 않은디? 그냥 쥐좆만한년 데려다 컨셉질 아녀?|1 +구글 업로드 속도 40~50mb 나옴|0 +개,돼지들은 저런말이 지들에게 해당 안된단고 생각함.|1 +2D여고생 젖탱이가 더 꼴림|1 +법이 개젖같노 ㅋㅋㅋㅋ|1 +이거 다운어케받아요..?|0 +진짜 주제 모르고 존나 나대네 ㅋㅋ|1 +증거 없고 소문이 있으니 소문이 있다 말한것뿐 그 이상도 이하도 없습니다|0 +성형했냐? 눈깔이 다르네|1 +나는 지금 푸꾸옥 여행중인데 ㅋㅋㅋㅋㅋㅋㅋ|0 +년초부터 액땜 제대루 하네요 강사님 파이팅~~ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +셋다있음 심지어 비례한국당도 있다 ㅋㅋㅋ|0 +문재인이 뒤를 봐주고 있는데 잡히겠냐?승리도 글쿠|0 +디지털이 발전하면서 예견됐던 일이지. 근데 여자가 먼저 찍자고 하던데...|0 +https://www.google.co.kr/amp/www.donga.com/news/amp/all/20190625/96168051/1선동이다 3심에서 유죄라고 다시 2심내려보냄|0 +병신이 가치높일시간에 그년 따른남자랑 앙 오빠하면서 질꺽질꺽 하응하응 거린다. 뭘 활용할 생각을 하니 ㅋㅋㅋㅋ 세상이 뭐 너에게 진취적으로 나아갈길을제시하는것같음? 그거 다 니 착각이다. 인생에 규칙은 없다. 니가 그렇게 생각하건 말건 니자유임.|1 +노가다 힘들어서 못버티고 가는 사람들 많다 개병신아|1 + 한 말을 그대로 글로 적는 것|0 +개인적으로 궁금한게 일이 좆같거나 사람이 좆같으면 직장을 옮기는게 맞는거아니냐? 왜 자살을함? 자살할 용기보다 이직할용기가 쉬울꺼같은데...;|1 +니말이 맞다 ㅋㅋ|0 +a는 컵이란 말도 쓰지말아줬으면함|0 +중간에 수간생 뭐야|0 +@시계는아침부터똑딱 윤지오같은 애들하고나 어울리는거 보면 원래 개념없는 년인건 확실|1 +죄다 거지들 차|1 +백프로 ㅋㅋ |0 +단지내 수영장있는 아파트 관리비 가 비싸더라.|0 +스타워즈 클론 전쟁 씨발년아|1 +씨발넘들|1 + 게이애 침착하고|1 +전라도 뺴먹지 마라... 요즘 좀 안쳐맞더니 간이 커져서 대한민국을 망치려 든다|1 +시작부터 이미 그렇게 써놨는데 난독은 무슨|0 +오..|0 + 자영업자니?|0 + 제대로 보고해도 망치는데, 감으로 한다고? ㅋㅋㅋ|0 +불가능함 ㅋㅋㅋㅋ인건비싸니까 외국제조업체들이 잠깐 들리는거지지들잘나간다고 착각하는거고관광? ㅋㅋㅋㅋㅋㅋ 그걸로는 한계 전세계 정액받이들러리밖에안됨한국도좆도없는데 그러니 박정희가 뛰어난거다좆도아닌국가에서 산업국가로 뛰어넘은게그게 50년 당시 더 잘살던 베트남 필핀 태국 과의 클라스의차이지|1 +슨상님ㅠㅠㅠ|0 +주작무새들이 간과하는게 진짜 세상엔 별의별놈 다있음. 자기가 못봤다고 다 주작이 아니라 말이 안될거 같지만 저런일이 있을수 있다는 가능성을 열어둬야함.|1 +요양병원은 의사면허있는사람 못 구해서 한의사 치과의사 데려다 당직시키는데|0 +쏴리 엑소 인기맴버 첸이 일반인과 과속으로 인한 결혼소식을 공지했어|0 +9등급은 해석못해|0 +졸커 ㅋㅋ|0 +갤럭시북 플렉스 샀다. 존나 좋은데? 그냥 거지새끼 아니냐?|0 +수영복사진인줄알고들어옴|0 + 3.1운동 안 나서면 불을 질러? ㅋㅋㅋㅋㅋㅋㅋㅋ|0 +정치병자틀딱ㅈㅎ|1 +쌀국수 빼고 우리보다 나은게|0 +월세도 못 내는 놈한테 칼춤 한번 처주고 평생 깜빵에서 살려고 ㅋ|1 +따면 개이득이이고 꼴면 세입자힌테 미안해서봐주게됨|1 +ㅇㄱㄹㅇ 난 다 남자형제고 와이프는 다 여자형젠데 가끔보면 그냥 존나 답답할때가 있다. 장인어른에게 쿨함과 남자다움을 바라는건 아니지만 확실히 내 딸 고생시키지말게 라는 은연중의 진지한 느낌 풍기면 기분 굉장히 안좋다. 남자든 여자든 다 공평하게 귀한 자식이라는 개념이 많이 부족함을 느낌 |0 +누워서 가도 된다|0 +얘 특징) 옛날에 이승환 팬이었음|0 +외노자게이였노|1 +정봉주 같은애가 국회의원을 했다는것만으로조선인 수준이 적나라하게 드러난거지|1 +곧 있음 그들이 베들베들 거리며 몰려 오겠군요..|0 +태권더박 |0 +속보! ㅈ댔다! ㅋㅋㅋㅋㅋ ㅁㅈㅎ|0 +A : 마스크쓰면 이쁘네!|0 +ㅇㅇ... 섹스가 의무적이 되는것..정말 힘든 일...동거는 진짜 해봐야한다.|1 +그런 케이스는 아직까진 결혼하고싶지 않나보지우리나라 연예인들도 그 연령대라도 결혼 안하는 경우 제법 되고|0 +카케구루이 드라마 같은데|0 +주예지 걸레아니다 말함부로하지말아라...|0 + 문재앙 정권과의 커넥션을 찾는 수사처를 지맘대로 폐쇄하는 걸 말하는 거니까|1 +제발 좀 조용히 있자!!! |0 +저때는 안전이라는건 생각도 안하고 할때 아니냐|0 +Jk|0 +점이 왜캐많노?|0 + 독재 -> 독재 뭐? 박정희가 진짜 독재자였으면 그때 김대중 김영삼부터 숙청했음. 그리고 나는 국민 잘먹고 잘살게만 해주면 지도자가 독재 하든말든 신경 안씀.|0 +십년묵은 체증이 내려감 꺼억~|0 +지지율 3% 면 한 석 얻겠네 ㅋ|0 +밑져봐야 본전이니까 ㅋ|0 +양심을 걸고 성형외과 피부과가 가치를 창출하냐? 그냥 경제적인 부가가치 말하는 게 아니다.|0 + 그럼 그 정도가 같을 것 같냐?|0 +이혼녀 남친 빙의했지머 일게이수준 뻔하제 힛!|0 +원하는데로 해줌|0 +모발이식 틀딱들이 하면 가끔 붓는 경우도 있긴 함|1 + 전두엽에 브로카 영역, 좌반구에 베르키네 영역이 있는데|0 +뭔개소리야 진짜 한국인 자체가 병신이라니까 남자든 여자든 틀린걸 개선시켜려는게 목적이 아니라 그걸 빌미삼아서 인격비하하고 우위에 서려는게 종특인데|1 +ㅈㅈㅂ ㅁㅈㅎ|0 +과학이다 하는데 팀으로 만나면 얘보다 티모, 베인, 마이가 더 ㅈ같음. 얘는 라인전쎄고 한타도 좋고 캐리력도 최상급. 근데 캐릭터 설계부터가 던지기 좋게 만들어서 잘하는 애들도 한번씩 던지더라|0 +조선놈이 미국시민권 재미교포한테 개기는거 보소. 너 영어 첨부터 다시 공부해야겠다. 그지같은 또라이야. 어디서 영어도 제대로 못읽으면서 선동질이야. ㅁㅈㅎ.|1 +니엠착한불고기|1 +행복회로 그만 돌려라 !! 문제인 옥바라지나 똑바로 하고|1 +일본 사람입네까?ㅋㅋㅋ|0 +재능인데 노력으로 보이는 마법아님 ?|0 +지지고 볶고있었단건가?|0 +ㅁㅊㄴ ㅋㅋㅋ|1 +시체를 야산에 유기하려고 끌고 올라갔다가|0 +채영보다 천배 이쁜데 채영을 갖다대노|1 +TV에서도 저렇게 지웠다 넣었다 할 수 있는 자유로운 사회분위기 카~ 역시 갓본형님|0 +자식 잃은 부모의 슬픔의 깊이를 어찌 헤아릴 수 있겠습니까? 다만 하느님이 사랑해서 일찍 데리고 갔다는 종교적 신념에서 나오는 발상과 발언 자체를 납득 해야 하는 건가요? 저는 납득할 수도 없고 이해할 수도 없네요. 국민학교 시절 그 어린 나이에 들었을 때도 충격이었습니다. 본문의 내용과 비교가 보는 이에 따라 다를 수도 틀릴 수 있겠지만 그런 신념을 저는 꼬집고 싶은 겁니다.|0 +할부원금 0원인데 69요금제가 69000원이라고?|0 +내 꼬랑내를 안맡는건 뭔가|0 +등신새끼|1 +서대문 어떰?|0 +@반일반북 |0 +편의점 알바 존내 고소득직 아님.|0 +정신적으로 쇼크 받았냐?잘아는 변호사 알려줄께 손배청구 갈까 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +내가 말하는건 그냥 조금 짜증나는 마음으로 전화해서 환불요구나 다른요구도 없이 음식물을 쓰레기봉지에 버리는 사진까지 올리는 상황을 말하는거임|0 + 그리고 나 페도 아님 ㅡㅡ 그냥 일본엔 이런게 있다는게 충격적이고 신기해서 퍼온거임|0 +와 이 정도면 뭐 ㅎㄷㄷ 친구들도 못 알아보겠노 ㄷㄷ 살도 덤으로 빼고 화장하고 헤어도 바꿨네 |0 +청소기 시발 극혐이네 다이슨 미만 전부 짭 |1 +이정재: 아~이러면 나가린데...|0 +아 니미 첨 알았네이거 누가 갈켜준적도 없고..책에는 있었겠지만 오늘 첨 알았어|1 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ말그대로 개판이네|0 + 7년 대환란 ㅡ> 휴거 ㅡ> 오랜기간 방치된 지상 ㅡ> 진정한 천년왕국 건립 아니냐?|0 +개독아 배워라~|1 +이게 맞는 말이지|0 +그래서 어디서만나는데|0 +대기 없음매일매일 출석 도장 찍다가 입고되면 사는 거임그래서 시계가 주인을 고른다는 말이 나옴|0 +말만 들어도 역겹다 아무리 세상이 살기좋아졌다지만 인간이라고 다같은 인간이 아님 ㅉㅉ|1 +ㅇㅇ 어릴때 해봄|0 +ㅋㅋㅋ 애쓴다 ㅉㅉ|1 + 남들 신경 안쓸거면 4-5만원 내고 옆좌석 두개 전부 사버려라|0 +감사합니다|0 +일상생활이나 직장생활에서 말이지|0 +다음 생에는 갓남으로 태어나라 갓남 찐 장애인으로|1 +술이식기전 ㅂㅅ아|1 +그래도 또 뽑아주겠죠?|0 +군 생활의 고통을 공감하기 위해서 입대하는 여친은?|0 +일베버러지들아|1 +들어와 봤자 도와준 정치인 새기들 더 시끄러워짐 |1 +노령연금 무상진료 혜택보는 개돼지가 세금내는 우리보다 훨씬많아서 글쌔다|1 +사표 던지면 빨갱이만 더 좋게 해주는 거잖아|1 +기술 진짜 끝내주네 하지만 난 100만원 정도 티비면 만족함|0 +도둑한테 개꿀일 수도 ㅋㅋㅋ 사람 저렇게 많으니 주변 어슬렁 거려도 수상하게 보이지 않을 듯|0 +병신새끼들 ㅋㅋ 조센은 뭐 깨끗한줄아네 짱퀴벌레랑 동급이면서|1 +모솔들 많이 있겠네|0 +이국종 교수 급의 중증외상전문의를 말하는 거겠지. 석선장 때는 국내에 총상을 치료할 수 있는 의사가 별로 없어서 그때 막 이국종 교수 이름이 떠오르고 그랬었다.|0 +근데 수학을 잘한다는게 수학 자체가 실제로 쓸수있다가 아니라 그런 쪽으로 지능이 좋다는거자나|0 + (소련 붕괴, 아프리카/카리브/태평양 지역 신생 독립국들 급증)|0 +병신동네에서 알바나 하는 주제에 이런글 쓰면 뭐 되는 거 같냐너나 걔네나 똑같이 미개한 놈들이지|1 + 글쓴거 보면 ㅈㄴ 탐정도 아니고 공부해서 무슨 의미인지 맞춰야해. ㅅㅂ|1 + 형 50대 미시까지는 먹을수있다|1 +150,000만원 무식한 새끼는 ㅁㅈㅎ|1 +서울 집값 씹창나면 탈조선감|1 +저게 빅데이터를 활용한 관리다 어떻게 저런걸 사람이다 분류하겠냐? 야동이어도 파일이름에 근친 가슴 누나 봊이 이런거있으면 바로 컴퓨터가 걸러내는거고야동을저장해도 다른이름으로 해야함|1 +100% 일베충|0 +근데 인서울 4년제 기계과 같은 과도 용접하지않냐?|0 +라면은 모르겠는데 햄버거는 맞음빵 = 탄수화물고기 = 단백질양파/상추/토마토 = 비타민 담당여기서 토마도 한장 끼울거 두장 끼우고 등등 채소 더 넣고감튀 콜라 안먹으면 건강에 안 좋을거 하나도 없음.|0 + 자 이거 책임지셔야죠 캐삭빵 가자니까요 한국출판사 번역된거 인증으로 ㅋㅋ 겁나요? ㅋㅋ|1 +메퇘지년들한테 놀아나지좀마라 저능아새끼야 재미도없고 존나 억지구만ㅁㅈㅎ|1 +뭐가 다른지 설명 안해주는게 특징|0 +제발요!! |0 +근데 저거 검색해보니까 일본에서 아이들이 밖에서 늦게까지 놀러다니지말라고 부모들이 지어낸 이야기노 ㅋㅋ 시발|1 +대한민국 주적은 청와대와 국회의원들 정치인들이다. |0 +텔레토비 이후로 이렇게 흥분한 적 처음이다. 하앜|0 +마른 글래머 ㅇㅂㅇㅂ|0 +저 정도면 취미로하는 새끼인듯 |1 +그러게... 요새 짱깨들이 일베에서 분탕을 존나 치는 듯|1 +도대체 질의 차이는 뭘기반한 구분인지 알수없지만 환경이라면 동의|0 +뚱녀는 좆을 진짜 닭다리 빨아먹듯이 빨음 정액도 잘먹음 그외엔 무쓸모|1 +안밝히는데!|0 +이게 맞지 시발 무슨 광고영상편집자도 아니고 유튜브 영상 편집인데 얼마나 개 씹 고급 영상 만든다고?... 돈 많이 벌면 시급 많이 줘야한다는 개 좆 논리는 어디서 튀어나왔지; 못배워먹은새끼|1 +난 스페츠나츠 요원이다 이기|0 +ㅈ까는 소리하네 ㅋㅋ 걔네가 하는 땜질이랑 노가다 김씨 땜질이랑 같냐고 니 말하는게 지잡대 까니까 지잡대 재학생이 의대생은 공부 잘하거든? 이거란 똑같은 소리임|1 +저게 선생이야. 경찰 뭐하나? 기데기한테 풀어버려|0 + Kelly Sanger|0 +거기서 한 9년차되면 영주권신청하던지|0 +틀린 말 아니던데|0 +남양주나 남양이나 개돼지들|0 +두루킹 뭐? 잘못이냐고? 잘못이지 ㅋ 어쩌라고|0 +아다노|0 +편돌이주제에 ㅋㅋㅋㅋ ㅅㅂ 누굴 평가해 ㅅㅂ거 |1 +섹스|1 +미국이 잘도 미개한 똥양인 반도새끼들을 51번째 주에 넣어주겠다ㅋㅋㅋㅋ한치앞도 못보는 병신 개돼지들 투성이에대표로 뽑은 새끼가 문재인ㅋㅋㅋ 수준ㅋㅋㅋㅋㅋㅋㅋㅋ|1 +아따~ 거시기허네~|0 +에휴...대구빡들....|1 +솔직히 박그네 탄핵시 여론에서 담배값 인상이 상당부분 영향을 미친 건 맞다|0 +자율주행하나로 밥벌어먹는 테슬라가 맨끝 ㅋㅋㅋ 신뢰성이 좀|0 +본인, 준틀딱이라 도저히 이해가 안가는데... 일종의 놀이문화 아닐까?... 펭수도 그렇고... 모르겠다|1 +미국 나름 오래 살았는데 CC 근처에 가본적이 없어서 학비가 3500불 하는 학교가 미국에 있는 줄도 몰랐네 ㅅㅂ ㅋㅋ 컴칼다니면서 저러고 있으면 ㅆㅎㅌㅊ 인생인데 ㅋㅋ 그래도 차라리 한국에서 일베나 쳐하는 놈들은 돈이라도 안쓰지. |1 +모유강강물추천좀 ㅇㅇ|0 +1등세입자님이네 |0 +진교수! 상대할때 빡치는데 같이 편먹으니 탱킹 짱임!|0 +네다음순시충|0 + 알릴레오 뉴스공장도 구독할거임|0 +의사의 사명감 물론 중요하지만 그것보다 먼저 의사들의 이윤이 추구되어야 한다.|0 +카트리지로 살까 e숍에서 다운받을까 아 고민됨|0 +니 잘못인데 왜 좆같노... 이제부터라도 좋은 삶을 살도록 노력하면 되지|1 + 보내줄꺼 보내주고 이제 시작하는일에 더 집중하는게 맞지 뭘 ㅋㅋㅋ 시발|1 +미안해 ㅠㅠ 잘 먹을게|0 +맞는말하면 논란이되는세상|0 +호남향우회들 열렬한 지지 남평문씨 북한홍어 좆재인|1 +무슨 영화에서나 봤을거같은 벌레들 많이 볼거다|0 +눙물이..보고싶다..노무현 형님....|0 +도움되라고 하는게아니라 본인 뿌듯하려고 기부를 하니 저리되지|0 +4칙연산 공부중|0 +다시 생기면 의치한약수|0 +갑자기 칼(부)림|0 + 너는 지금 돈없는 대학생이 맥북쓰며 자 위질 하는걸 역겹게 보고 나서 나도 그런 사람이라고 단정짓고 나서|1 +쓰레기들이 쓰레기통으로 자동 분리되었군|1 +사다가 구워 먹으면 되는데그것조차 귀찮은건가? |0 +지 딸한테 혀넣는 놈도 있다는데 이것쯤이야|1 +젊은 아줌마들 저런사람 많다 악랄하지|0 +나도 아버지 원주세브란스 입원했을때근처 모텔에 며칠 투숙햇는데소리 들리더라... 햐..꼴리면서 뭔가 자괴감들고... 암튼 슬펏음 ㅅㅂ..|1 +이국종도 지난번에 욕처먹을짓 했잖아이재명 탄원서 제출한거 ㅋㅋㅋ난 그거 보고 아 저분도 그짝이겠다싶었다|1 +우리도 홧병으로 죽겟어요... 당신때문에...|0 +나름 아끼던 프레임인데 ㅠㅠ|0 +배성재 저놈이 저러니깐 더 이상하다.논란되는 애들 존나 칼같이 컷하네 ㅋㅋㅋㅋ|1 +저정도면 몇 kg냐승ㄴ170이라고 할때|0 +주식은 않하는게 돈 버는건데.....|0 +시대를 감안하고 보는 영화가 있고 아닌 영와가 있는데|0 +ㄴㄴ 10년 짬으론 아직 비비기 힘듬|0 +글고 엤날에 종합 이벤트 경기로 웰터급 3류선수랑 하리랑 붙은적 있는데 하리 그라운드에서 순삭 당했음ㅋㅋ|0 +거의 100알 정도 되더라|0 +I can do this all day|0 +보증금 없다고 안주는 거지 주인 새끼들도 존나 많음돈 앞에선 다 거짓이고 눈탱이야 ㅋㅋ|1 +ㅇㅂ는 주는데 글이 좀길면 나눠서 올리더라도 위치하고 가는길 상호정도는 알려줬으면 좋겠노 그럼 나중에 찾아가기도 수월하고 정말 좋을텐데 그래도 한가지 알게된거는 안동은 짜다.이거구만 물통 달고 다녀야겠노 |0 +넌 시발 뭐하는 새끼인데 여기서 이지랄이냐? 기술이나 배워라.|1 +쓰레기들|1 +아줌이 왜 좋냐?|1 +쓰레기새끼들이다 진짜|1 +사실 희생정신 있다고 의대에서 내신1등급 따는거 아니고 노벨의학상 따는거 아니잖아.그냥 잘난 놈이 사람을 살리는거야.그뿐이야|0 +찌찌 오랜만에 들으니까 웃기당|0 +그런건 어디서 무슨 과정 배워야 되냐? 코딩인가 뭔가 그거냐? 아니면 포토샵?|0 + 병당 3만이면 평타니까 사서펴라|0 +진짜 비행기만큼 시행착오많이 격은 대중교통이 있을까?창문 네모낳게 만들었다가 모서리 크랙가서 공중분해.동그란 창문안에 구멍조금 안뚫었다가 압력으로 공중분해.짐 넣는곳 뚜껑설계미스로 공중분해60년대부터 70년대까지 여객기 베타테스트였음.|0 +야수는 ㅇㅈ이지|0 +화정인가요?|0 +중일에 언론이 있어???ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +현타|0 +그 또한 지나간다 마음을 조금 내려놔 봐라|0 +우웩|0 + 국민들이 빡치면 어쩔건데?|1 + 이재명이 이번이 처음이냐?|0 +대견 하긴 한데 젊음이 아깝다 이쁠때 꾸미고 즐기다 나이들어서 해도 되는데 얼굴 손 다 망가지고뭔가 비효율적으로 보이는|1 +설마 합방까지 하자고 하겠나하자고 하면 주제넘은 짓이고|0 +칼로 죽여야 살인이냐.. 주뎅이에 병균 쳐 넣고 돌아다니는건 세균전이라 생각이 든다.|1 +추스리고 누워야 하는데 대구, 경북 |0 +파티는 무슨 옥탑방에서 가스렌지놓고 캔맥까면서 삼결삽처먹는게 파티냐 븅신새끼 파티는 클럽이나 호텔가서하는게 파티고 니새끼가하는건 궁상이다|1 +제발 사형시켜라 좀|1 +안써봐서 모름 ㅁㅈㅎ|0 +골든 래브라도 잉글리쉬말곤 잘 모른다이기|0 +둘다 해당되는거지 지금의 신생아들이 30년 정도뒤에 현 3~40대 틀딱이된 정년퇴임자들의 연기금을 매꿔줘야하는데 인구절벽 때문에 그럴 가능성이 거의 없고 그거에 맞춰 법 개정이 될거야 이대로 가면 답이 없음지금부터 아끼지 않으면 시간이 흐리면 흐를수록 더 좆됨|1 + 좆급식부터 철딱서니 없는 20대 초반 여자들이 |1 +김치는?|0 +이제 버러지들 몰려와 신고하여 블라인드 만들겠죠. ㅠ|1 +이래서 스스로 걸리거든 "죄인들이"~ㅎㅎㅎ 재미보고 털리고 그치~~~?|1 +할말이 없노 ㅋㅋㅋㅋ 아이큐 두자릿수 예상한다 |1 +근데 안산이면 조선족한테 칼빵 맞기 좋은 동네 아님? ㄷㄷ 방검복 필수로 착용하고 다녀야겠네.|1 +뭘잘못함?|0 +조선의 천박한 의식은 바로 '공부'에 대한 개념 인식에 있다.조선에서 공부란, 말 그대로 공부를 위한 공부일 뿐. 국영수 시험쳐서 점수 매기는 '수험'을 공부라고 생각하는 아주 천박한 의식이다.용접을 하기 위해, 전기나 가스의 특성을 이해하고 금속의 성질을 터득하며 용접행위를 통해 생활에 유용한 여러 가치를 창출해 내는 그 모든 과정이 다 '공부'다.근데 정작 조선에서 공부를 잘한다는 사람들이 국영수 배워서 남들에게 유용한 도움을 주는게 뭐가 있냐?수험으로서 배운 국영수가 대체 사회와 공동체, 국가에 어떤 창의적이고 실용적인 도움이 되는거냐?저 여자강사처럼 기껏 수학 배워봤자 인류 발전을 위해서 쓰는게 아니라 그저 애들 수학 '점수' 높여주는데만 활용함. 그게 다임.정작 현실에 어떤 실용적으로 활용되는게 아니라 그저 시험의 '점수'만 높이려는게 이 천박한 조선의 공부다.이딴 썩어빠진 나라에 무슨 창의력이 있고, 기술 발전이 있고, 문화 발전이 있겠냐.그저 선진국들 시다바리나 하면서 평생 하청이나 받아 쳐먹고 살아야지.. ㅉㅉ |1 +저 말뜻은 표주면 주고 아니면 그 돈으로 신청사나 짓겠다는 의미|0 +닭고기 맛|0 +천진난만하고 좋네|0 +높은 수요로 인해 실력이 없고 사명감만 뛰어난 병신들만 의사로 남아있겠지.|1 +내가 도요다 RAV4, 혼다 파일럿 키아 텔류 현대 저거 이렇게 시승해봤는데펠리가 잴 딸림|0 +비교야 안 되지만 부부교사 퇴직후에 한 달에 700 이상 들어오나보더라|0 +근데 장난 농담 때리노 ㅅㅂ련아 표현 같은 소리 하고 있다 표현은 ㅅㅂ 에로DVD 작가한테 가서 표현배우고|1 +난 인터넷 접수후 10일후 됐다고 전화와서|0 +아르유베다 안모이면 그게 회식인가요? 혼밥이지?|0 +태생자체가 천박한 전라도핏줄ㅜㅜ..굳이 여기까지와서 천성인 분탕질ㅜㅜ...여기온 전라도 새끼들은 맺힌게 많은듯ㅜㅜ|1 +비싸게 판게 아니라 정가에 파는게 뭐가 문제냐? 게이는 무슨일 하길래? 지인한테 싸게 사려는 기생충 집단이 되지말고 이왕 같은가격이면 지인을 도와준다는 마음으로 사주는 리더 집단이 되어보자 가령 식당가서 사장이랑 친하니까 깍아달라 노가다 뛰는데 친하니까 일당좀 깍아서 해달라고 하는거나 마찬가지임. 친구는 자신의 일에 자부심을 갖고 일하는것 같은데 뒤에서 욕하진 말자 진심으로 대하면 언젠가는 다 돌려받는다 게이야|1 +하긴 카페 첨부파일도 다 검열하던데 ㅋㅋ|0 + 이거 무슨 아이디 같지 않아요?|0 +미신당에서 금태섭이 데려다 싸던지...|0 +양이 200그람대..난 걍 편의점 도시락이 양 많아서 좋더라..|0 +2주전만해도 나경원이 조금 앞섰고 전문가들이 동작을 지역구 60대이상 인구가 늘어나서 힘들꺼라고했었고|0 +캡쳐잘했노 틀딱새끼들 스크롤캡쳐도못함 뇌가 풍화되서그런가 ㅇㅂ|1 +에이스네|0 +좆문대도 잘만가더라 ㅋㅋ|1 +지금 hp가 몇년째 1위야|0 +걍 해본말인데 또 발기하노 ㄷㄷ|1 +엄청차깁고 물살쌔서 거의다 죽는다더라 암튼 자살명소임|0 +이거 니들이 운전면허 필기 시험볼때 공부 안하고 쳐봐서 그래시발 나는 공부 존나 해서 저런거 까지 아직도 기억한다|1 +쿠마몬 컨셉 배끼는것도 방송하면 재밌겠네 ㅋ|0 +그닥|0 +야임마 내친구 소방관인데 시골이라가 불안나고 맨날 대기중인데나이 서른중반에 월 세후 사백넘게가져간다등신들 뭣하러 사기업다니노잘릴 걱정없지 이제 국가직되면 혜택 개쩔지사기업 좃소 사장 직원 개새끼들아열심히 손발 잘리고 야근 좃빠지게 해가수출 많이해서 세금 많이내라내친구는 얼굴펴가 차도 벤츠 e300타고일도 개편하지소방관이 그렇게 꿀인줄 몰랐단다힘든척하는거 진짜 가끔이벤트라는데조선소 기타 제조업 사망사고비하면 워라벨 개쩐다고함좃소 대기업 직원들 평생 인생 갈아넣어라 ㅋ나도 소방관되어서 개꿀빨아야지|1 +맛없음|0 + 어차피 유튜브서 주는거고 한국 사람이 돈주는것도 아니면서|0 +더뉴파사트가 가성비갑임 소나타k5에비비는크기의 전륜세단에 신차가3050으로뿌려서 1년된차 2천800안으로살수있음 이력없고완전무사고 주행거리만안팍 지금600할인중인 어코드인가캠리 3천초반에신차가능하고|0 +용접 좆같은거 맞잖아?용접이 세상 사람들 전부 하고싶은 꿀직장이냐?몸에 해로운 노동직이잖아? ㅋㅋ|1 +네다음 잠홋|0 +벌레공무원 월급노예들보단 나라에도움됨ㅋㅋㅋㅋㅋㅋ|1 +자식들이 볼까봐 걱정이네.|0 +댓글 중 "선동 당해서 촞불든 개,돼지 홍어들도 단죄를 받아야 할 공범자들이다"에1000000000000000000000000000000000000000000000000000000000000000ㅂ!|1 +서비스 이용하는데 돈 내야하는건 당연한거고,그걸 포괄제로 받다가 가격인상요인이 많으니포괄안하고 개별로 받겠다는데 왜 지랄임?결국 배달료 이후 배달사업이 존나 커지고소비자는 집에서 대부분 사먹을 수 있게된게 팩트임.빠바 생기고 동네빵집 망했다 선동수준이네 븅신새끼|1 + '일본이 모든 음식의 기원한 곳이라고 한거 너잔아 씨발년아'|1 +신천지 니들이 반격하면 할수록 |0 +브라운 전기면도기 오지던대 아침에 바쁜데 습식 면도 언제하냐 |0 +거지양아치dna 어디가겠냐|0 +진실됨이 느껴집니다. 화이팅!|0 +NLL같은 주제로는 진중권이 변희재 이기기 힘들어, 다른 주제는 모르겠지만|0 +일단 니부터 접어 씹새야 일베는 내 마음의 고향이자 내 마음속 친구들임 그게 쉽게 끊어 지겠냐?|1 +쟤들정 하나랑 결혼하기vs아다 메갈년이랑 결혼하기|1 +삼성전자 부장으로 퇴사했는데 떡볶이 장사한다고?? 말이 됨? 글로벌 초대형 기업 디렉터급 출신이 퇴사했다고 할 일이 그것밖에 없냐 좃나 무능한가보네 |1 +가|0 +감옥은 일게이가 간다...|1 +그 대표적인 예가 문재인|0 +벌레들 불쌍해서...우짜냐|0 +하지만 웃긴건 진짜 사회주의 사회가 되면 저런 실력있고 착한 의사가 안 나온다는거지 ㅋㅋ 실력있는 의사가 안 나오거든 ㅋㅋ 유럽이 딱 끄짝이지 ㅋㅋ|1 +ㄴㄴ 그냥 해논 닉이도 구로디지탈산다 키 잘생긴편인데 머하노 인물아깝게 인생짧아|0 +무현이는 인권없는겨?|1 +싼타모!페리|0 +쉼표 너무많이써도 안좋은건데 남용하네|0 +무죄 무죄 무죄 ~~1!@@~!!|0 +암튼 도살장 가는건 귀신같이 알더라|0 + 비싸면 시켜 먹지말고 가서 사가지 와...|0 +밍하니깐 좆성이 존나게 투자 하는거지 증설 증축하고?https://m.post.naver.com/viewer/postView.nhn?volumeNo=27068083&memberNo=11919328&vType=VERTICAL|1 +일단 집이 흙임. 그리고 내 직업이 안정적이고 괜찮기는 한데, 그렇다고 민간 대기업처럼 고연봉도 아니고|0 +검머외 외국인들 뮨재잉 찬양하는 인간들 많지않냐??? ㅎㅎㅎㅎ 뒤통수 거하게 쳐맞았네 ㅎㅎ|1 +쓰신 내용봐서는|0 +홍성 핑크당 만뽑아주는동네|0 +사형보다 더 큰 벌이네|0 +인동|0 + 근데 그 막줄이 틀림. 남자 ㅅㅌㅊ가 소극적이잖아? 여자들이 존나 좋아함. 지만 보고 딴짓 안할것 같아서|1 +물론 일부 입니다 씨바|1 +그때까지 ㄱㄱ|0 +12살 되면 뒤짐|0 +다 보험처리된다|0 +넘 귀여워|0 +카페에서 보면 서킷 많이 타시는 분들은 캠버 조절 하시는 분들이 많으시더군요|0 + 기록을 찾아보면 한국도 식인이 있었을거다.|0 +Anonymouth |0 +훗 ㅋ|0 +개소리하면서 도망가지 말고 근거 가져와 씨발 새끼야 ㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +머리숱에 인색ㅋ|0 +fetcherx 여기에 n번방 노예년들 종목별로 다있음 죄다 개씹좆중고딩년이 대부분이고 자1위, 몸에 좆집 암캐낙서쓰고 끝 오줌싸고 먹기 유두 쥐어뜯기 이게 다임그나마 10명중에 한두명 진짜 개좆꼴리게생긴 민짜라는게 꼴림포인트 똥베충들이 대부분 몰랐다는데 그게정상적임 딸칠거리조차안됨|1 +현명하네|0 +난 인터넷이 발달되서 혼자 할수있는 취미거리 즐길거리가 많아지면서 연애에 대한 절실함이 줄어드는것도 한몫 한다고 봄|0 +젊ㅁ은애들은 노상관이네|0 +안동은 타지 사람이 와서 잠깐이라도 살곳이 못된다 텃세가 진짜 상상도 못하고사람들 존나게 틱틱거리고 불친절하다 장사하는 새끼들도 손님한테 서비스마인드 이딴거 없음 이건 지들끼리도 다 인정하는거고;;;ㅋ인심좋다는 시장도 가면 호객행위 이런거 거의 없고가격 물어보면 안살거면서 왜 물어보냐고 짜증냄헬조센 씹선비 고장의 진수를 보여줌안동 특유의 좆같은 분위기 안살아보면 모름뭐 안동토박이 지들끼리는 집성촌 양반입네 자존심 가지고 잘사는거겠지권씨 류씨 비율 확실히 많고 인구 16만명밖에 안됨..말만 시지 개깡촌임|1 + 니 의견: 그냥 좌좀을 만나놓고 모든 자영업자들이 그런거처럼 일반화하지마라|0 +저렇게 안하면 나중에 분명 다른 나라에게 뒤집어 씌웁니다.|0 +쑤레기들|1 + 여튼 요지는 젊은 사람들 은근히 tv안본다는거지|0 + 베이비부머 전부 뒤져야 가능한 일이다 한국에선|1 +개 븅신들 저래놓구선 반일? 불매? 아주 지랄들을 하세요반일은 뭐다? 정 신 병!!일본이 없었다면 한국이 지금 이 위치에 있을 수 있을것 같냐?모든 제품은 로열티 없이 배껴쓰고모든 기술은 불법채류라도 해서 배워오고모든 스포츠 경기의 목표는 일본을 꺾는다는 단 한가지를 목표로 하고잘 생각해 봐라. 한국인이 정말 감사해야 할 것은 일본이라는 나라가 아무 탈없이 한국 옆에 있어주었기 때문이다.그것도 세계 수위의 선진국으로 한국 옆에 있어주었기 때문이다.명심해라. 일본은 우리가 받아야할게 있는 나라가 아니라 우리가 마음대로배껴쓰고 훔쳐쓰고 한 것들을 갚아야 할 나라라는 것을|1 +피싸개새끼들 인권을 왜챙겨?? 니 메갈하냐?|1 +경남 남해쪽 통영, 거제, 남해, 사천 뭐 이런 동네일듯.|0 +예쁘니까 넘어가라~|0 +이민을 씨발 갈수있는 새끼는 이미 해외에 집사놨지|1 +일본은 이미 30%가 노인인 세계1위 노인국이다(한국은 2040년쯤 달성될예정) 그러니까 한국이 정확히 일본의 20년 뒤를 밟고 있는거지..일할수밖에 없음 전체인구의 3분의1이 논다는건 국가적으로 엄청난 손해임 |0 +괜히 서울시장 박한테, 대선때 문한테,국민의당 박한테,바른미래 유,손한테 등골까지 빨아 먹혔겠냐|0 +그럼 쓰레기 뭐 어떻게하라고 |0 +신속하게 다들 밖으로 나가서 대기만 하고 있었어도 대부분 살 수 있었을텐데...|0 +이런곳이 많아야 되는데 이런곳이 너무 적어 문제네...|0 +동성애 똥꼬충들한테 동성애는 되는데 근친은 왜 안되냐 하면 온갖 현학적인 용어 들어가며 그것과 그건 다르다고 장황하게 설명함근데 몇년이 지나고 아무리 생각해봐도 뭐가다른지 모르겠음|1 +순위조작은 자세히는 모르겠고 음원차트 주작은 엑소 팬들이 열심히 스밍 돌리는데 뭔 이상한 놈이 1위 계속 먹길래 엑소팬들이 그 1위 먹는 놈 차트 상승수 까보니까 밤만되면 조오오온나 올라감 주작한 새끼 털고나니까 4개월 전에 갑자기 급상승해서 1위한 새끼도 주작으로 올렸댜는거 잡아냄ㅋㅋㅋㅋ|1 +문색히 필리핀 안가노|1 +하여간 방송국 새끼들 나도 욕했었는데 미안하네...|1 +아 됐고 김씨 일절만해 내일 또 한삽 푸러 가려면 쉬어야지 안그래?|0 +저사람도 최소 월3천은 벌텐데꼴랑 9급하겠다고 저기 앉아있는 쟤네들이 얼마나 같잖아보일까|0 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +소속사가 개양아치였네 ㅋㅋㅋ|1 +같은생각 ㅇㅂ|0 +헤비구스다운 미만잡입니다 여러분|0 +ㅋㅋ아니 할배가 유리하면 왜 나보고 자꾸 꺼지라함? 앜ㅋㅋㅋㅋ|1 +하고 있다는게 느껴지네요..경제 발전도|0 +힘내라 조국 !!!|0 +그냥 민찌기계에다 갈아버려라.|0 +유익한 정보 ㅇㅂ|0 +우리집은 냉동고만 파먹어도 몇달 살긋던데..|0 +잘못한게 없으면 말도 제대로 못할게 뭐가 있냐 |0 +내 생각이지만 프랑스처럼 모두 셧다운 해야된다고 봄. 너무 오버하는거 아니냐고 할 수 있는데 이런 경우는 오버하는게 나은거 같음.|0 + 부들부들 댄건 너인데 |0 +지금 36만|0 +삼한제일검|0 +타고남 + 노력이라는데 일단 기럭지가 타고나서 ㅆㅅㅌㅊ|0 +우리이니 하고싶은말다해~~|0 + 거긴 지금 개중공애들도 많고 일단 홍어들이 모조리 이주해온 곳이라 위험해 청소년들도 최악이구|1 +예랑 예랑 하길레 여자 이름인줄 알았더니 예비신랑이었던 거냐?니미...|1 +잘생기면 이런일 안겪고 일사천리로 보지따먹고 집와도보지가 암말 안함|1 +저 여자 이름이 김세연인데|0 +이맛에 일베하는거 아니노|0 +원래 사람 뇌라는게 . |0 +스윙스사진 벽이랑 문쪽 굴곡봐라 ㅋㅋㅋㅋ 보정을 좀 티안나게하던가 저건뭐냐 ㅋㅋ|0 +주작 ㅁㅈㅎ|0 +진짜 땡기네 국수 ~~|0 +붐업|0 +원래 진중권은 미친놈만 보면 짖는 개 그 자체였음문재앙이 그래도 멀쩡할 때는 가만히 있다가누가 봐도 명백히 치매걸린 미친개가 되어버린 지금은앞장서서 짖어 댐노무현도 살아 생전엔 미친짓 했을때진중권한테 존나 욕처먹고 노빠들이 진중권 극딜 깠었대|1 +딸치는곳 밤꽃냄새 물씬풍기겠노|1 +통일은 언제되나요?|0 +입소 기념|0 +시간을 낭비한 죄|0 +바로 등록 했습니다 ^^|0 +저런소리할때 민망하다는 생각은 안하는건가|0 +ㅋ|0 +슈마훠 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +난데 나도 키 165 ㅋㅋㅋ 머리만 반삭임|0 +쳐 웃기는|1 +박근혜때 진박 마케팅 생각나네|0 +매가 약인것을|0 +너는 어떻게 해야 구원 받는지 알고 있냐???|0 +남 앰 타령하기전에 니 애미나 신경 좀 써라|1 +저게 나라냐|0 +2014년 씨발 뭐야|1 +주작같은디 그리고 성욕은 남자들의 당연한 욕구인게 맞죠 여러분들.ㅋㅋㅋ|0 +이제 이런 거 봐도 그러려니 함|0 +소오름. 내글보다 망상 오지네|0 +심각하네|0 + 실무로 한게 아니라|0 +파도가 존나 높지는 않은데 기본 교육 받기에 좋은 파도 너울|1 +김트루는 디씨네임드야|0 +지랄하네 뒤에서 처박아서 앞차가 더밀려나갓을수도있지 ㅋㅋㅋㅋㅋ|1 +9편에서는 거의안나옴 그나마다행|0 +야간자율학습... 모집병 자원입대...|0 +너같은새끼들만 그렇게 생각하겠지 닉값 존나 하네?|1 +암만 페미 욕해봤자 남자가 결혼 원하는 비율이 통계로도 더 높더라 ㅇㅇ 이건 그냥 지들끼리 경쟁하는 거 보고 페미가 몸값 올리니까 욕하는 거. 싸게 대주기만 하면 바로 신나서 엥겨붙을 것들이 한남.그러면서 상폐녀가 어쩌고 남자는 능력이 있어서 혼자 늙어도 괜찮네 어쩌네.. 속내는 그냥 눈높이 낮추고 대충 결혼해달라고 바람 잡고 싶은 거고, 현실은 능력남은 따로 있고 지들은 여자보다 생활력 없는 천민.|1 +글수 0|0 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ개터졌네|0 +죽이라고 건너는건데....|0 +ㅇㅇ 지들끼리 돌려 사귐|1 ++일뽕역갤사회부적응자|0 +뭐야 존나 이쁘네 |1 +김어준이 진짜 사기꾼이지|1 +TK와 PK는 같은 경상도지만 좀 다릅디다.|0 +정당한 성인콘텐츠라는 말부터 틀렸어 자식아|1 +일본판 강형욱이 막장 시바견 교육시키는거보니까말 안들을때마다 존나팸. 막 때리는게 아니고 대가리 무릎에 끼우고 손바각으로 있는 힘껏... 나중엔 꼬리흔들고 말 존나 잘들음.|1 +20%가 계속 제곱제곱으로 나가는건생각 안하냐|0 +와 오늘 2번째 댓글달게하시네~~ 참 훌륭한 아내분입니다. 서로를 위하는게 느껴지네요~~|0 +국밥 아줌마는 많이 하는거 알고 있었고..(과거 개쭈글)|0 +꼬우시면 너도 공부해서 '의사'되세요 ㅋㅋㅋㅋㅋㅋㅋ|0 + 유투브 뭐 하나검색하면 수많은 리뷰어들 뜸 ㅋㅋ|0 +헌법조문 을 다시한번 상기했으면 좋겠습니다.|0 +너, 왜 나 괴롭히냐?|0 +주예지 발언 영상보고 왔는데 용접공 비하 발언이라고 하기엔 애매하던데?걍 공부못하면 기술이나 배우라는 의미더만.맞는 말이지 그건. 공부가 적성에 안맞으면 기술이나 배워야지이걸 용접공 비하라고 해석하는 건오히려 그렇게 해석하는 놈들 머리에 용접공은 하찮은 직업이라는 의식이 깔려있어서 그렇게 받아들이는 거라고 본다|1 +애니도 다 현실 반영이었던거네 ㅋㅋ|0 +감싸고 지랄이네|1 +하여간 존나 개병신같은 한녀네 ㅋㅋㅋㅋㅋㅋㅋㅋㅋ 손봐라 씨발 ㅋㅋㅋㅋㅋㅋㅋㅋ|1 +무려 국가기관에서 표절ㅋㅋㅋㅋㅋ씹ㅋㅋㅋ|0 +공지영, 김제동, 하리수|0 +신발머임 개이쁘|0 +사회혼란 조장하려고 무리수 던지는거임|0 +여성폭력”이란 성별에 기반한 폭력으로 신체적·정신적 안녕과 안전할 수 있는 권리 등을 침해하는 행위로서 관계 법률에서 정하는 바에 따른 가정폭력, 성폭력, 성매매, 성희롱, 지속적 괴롭힘 행위와 그 밖에 친밀한 관계에 의한 폭력, 정보통신망을 이용한 폭력 등을 말한다.조문을 왜 니 좇대로 해석함?|1 +저 남자새끼 관상이 너무 드러워서 안보는 프로|1 +경험담이냐?ㅋㅋㅋ|0 +나는 문재인이 임기를 채우지 못할걸로 보임|0 +폰파냐?ㅋㅋㅋㅋ|0 +그리고 초딩 중딩도 아니고 고등학생이 이걸 모른다?|0 +말하면 놀릴까봐 야그 안함|0 +"근본없는 탈북자 새끼가 어디서 말을 섞어 변절자 새끼"|1 + 베트남 여행지 추천해 달래서 푸꿕 추천하는데|0 +친한 여자사람 친구 신랑이 대깨문이었음친구가 아무리 설득을 해도 한국당 싫다고 못박던 사람이었는데어제 거래처 사장이랑 회식할때 거래처 사장이 문재인 욕을 팩트로 무장해서쏘아부쳤나봄 집에와서는 이제 문재인 정권 지지못하겠다고 얘기했다함 ㅋ|1 +엄마부대 청년단체|0 +우리팀이건 적팀이건 초반계속뒤져나가는 병신임에는 똑같지만 야스오가 사이드처밀면서 css 주워먹을 시간을 벌어주냐 4대5지냐 차이|1 +채널에이 나름 중도 아니냐?|0 +매너제로 어케 알았지? 당신은 천재?|0 +이미 스스로들이 사람이길 포기한 벌레들 이에요...|1 +회식이라잖슴??? |0 +귀엽네 나이도 어리구만 좀 봐줘라 돌대가리 용접새끼들아 시키는데로 지지는거밖에 못하는새끼들이 부심은|1 +트리차다는 진짜 존나 예쁘다..|1 +일1베에서 본 글만 보면|0 +지이이이잉|0 +그래서 나도 한번 시켜 먹어봤는데,|0 +광분한군중들이 마트에서 빠따사들고 몰려가겠꾼|0 +똥줄타겠네|0 +물총에 까나리 넣어서 옥상에서 쏘세요.|1 +지졋스 아니냐|0 +외노자 불체자에 아무런경각심도 없는 나란데|0 +절대 위에말 듣지마라 범죄다 실행하는순간 철컹철컹이야 명심해!|0 +ㅅㅂ 내 꿈이 진짜 저런 스몰웨딩이다사돈의 팔촌까지 끌어모아서 어색한 자리 갖는거 상상만해도벌써부터 기절할거같노|1 +음주운전이 더 망신아님?|0 +회원탈퇴 어떻게 하노 ?|0 +그냥 프레임 전환.|0 +제발 외질이랑 듀오하는거 보여줘|0 +저거 wrong되면 pray로 들어가서 die|0 +권영진 시장관련 이슈 같은건 관심도 없어요.|0 +저 1이라는 숫자가 너무 마음이 아프다 ㅠㅠ|0 +말좀 듣자~~|0 +그땐 부사관이 ㅎㅌㅊ 직업 누가하냐? 이 마인드였는 데|0 +선관위 수개표해라 전자개표 못믿는다 |0 +21세기를 사는인간들이 어찌이리도 미개할수가 있단말인가조선이나 대한민국이나 바뀐게 없네 |0 +에어팟 쓸려고 아이폰쓴다사실상 에어팟이 본체임|0 +부동산 부자는 죠져도댐 국가에 아무런 이득이 없음 그 대신 사업가는 좀 풀어주고|0 +보신탕집에 팔아라아니면 모란시장에 자리펴고 팔던가|0 +병신 홍어새끼야 일베는 박정희 가카같은 똑똑한 독재를 지향하지 씨발 지 꼴리는데로 북한 중국 독재 ㅇㅈㄹ 하고 있네 븅신 ㅋㅋㅋㅋㅋㅋㅋㅋ|1 +홍어새끼맞네|1 + 이말 어디가 이석기 옹호냐고 ㅋㅋㅋㅋ 그걸 말하라고|0 +예배를 안드리면 영업이 되지 않아 망한다는 표현을 저리 어렵게 하는군요|0 +통신요금연체 몇년지나야 소멸되냐?|0 +와 시발 고소영보고 슨상합성인줄 알았다|1 +너희 가족이라고 생각해봐라..|0 +븅신 ㅋ 어차피 개백성들은 선동하는대로 따르게돼있어|1 +진중권은 사랑입니다.|0 +이마니보다 조빱이네~~?? ㅋㅋㅋㅋㅋㅋㅋ|1 +뭘 중국을 까고 그래 그래봐야 조선과 별 차이가어 없구만 저건 조선에서도 흔한일 아닌가?|0 +구단주 조단은 재산 2조 아닌가 ?|0 +지가 패드립 치고 지가 열받고 ㅋㅋㅋㅋㅋ |1 +진심으로 토나온다 |1 +쪽빠뤼 차 몰고 다니니깐|1 +끼리끼리 몰라>??|0 +2년제지잡 경영 많던데|0 +랜선이 없나?|0 +이런새끼특징 지가 깨끗한줄앎 남들이보기엔 궁상따는씹찐따ㅇㅇ|1 +정상적인 연애 못해봤으니까 그런 환타지 쓰는거잖아.멍청아|1 +전설의 딸카게 사건|0 +정부 총리 고위관리에 무능이죠|0 +진중권이 좌파인데 바른말해서 유시민 문대앙 패거리 죄국이 아플거다우파가 옳은소리 백날해봤자 개무시한 좌파인데|1 +진중권 띄워주기 이제 ㅁㅈㅎ|0 +그리고 다시는 원래대로 안 돌아 감.|0 +패딩은 캐나다 구스 노비스 미만 씹잡이다.몽클 무슨너클 맥케이지 스톤 등등 다 꺼지고 캐구 하나만 있으면 된다 |0 +쓰레기들 정화좀 시킵시다.|1 +보르릉~ 보르릉~|0 +꼭 한국한테 열폭하는 나라들에서만 이런 일이 터지네? 일본은 잘 나가도 그러려니 하면서, 한국 잘 나가는 건 그리도 아니꼽냐? 지나년놈들이고 태국년놈들이고 다 우물안개구리에 무식이 하늘을 찌르더만.|1 +왜?? 사회 규범 규율 때문에?? 보지에 피흘리기 시작하면 생리적으로 임신가능 시작을 알리는거 아님?? 제일 좋을땐데 뭐가 문제야? 씨발 개같은년들 애비 등골이나 쳐빨아먹다가 결혼해서 지가 쳐먹고 쳐입던거 씻고 빨고 1인분 할려니 힘들어서 징징대고 우울증 쳐걸리고 개지랄 염병하는 좆쓰레기 헬조센 창년들|1 +틀니|1 +일베도 열심히 하고 |1 +금딸 하려면 원본 저장해야 되는 부분인가..진짜 개역겹네|1 +아님ㅋㅋ 가보기나햇냐|0 +안전거리미확보로 충돌한차 과실이 클듯|0 +자식 키운거는 망했고화분이라도 잘 키워보겠다는데..|0 +취하려고 마시는게 술이지 빡대가리야|0 +비교불가 용접이지|0 +둘다 닉ㅇㅂ|0 +40 50대도 찐따엿던거 티나냐 ?|1 +내 분신을 만들고 싶은 욕구가 잇음|0 +타겟시켜서 뒤진건 맞는데 난 솔샤르아래에서 반시즌을 말한거임|0 +지도 지겨운지 딴짓하다 걸리면 시간추가 ㅋㅋㅋ|0 +야 돈있으면 조선족한테 칼로찌르라고 시켜|1 +씨발 이걸 모른다고?면허 반납하자|1 +보통 독서실 총무 들어가는건 다들 20만원 30만원 받는거 알고 들어감그래서 그냥 잘해주진 않아도, 좆같이만 안했으면 저사람도 신고하고 최저시급 적용한 월급 받으려 하지도 않았을거임 선을 넘어서 굴렸으니까 저렇게 된거지 |1 +아구. 저 쓰레기.|1 +물량이 없어서 못팜|0 +EYAN-143|0 ++국민들이 진짜고생하신다고 꿀뿌려줌 예를들면 국가직전환|0 +에휴 유사 씹쓰레기 국가 수준 ㅋㅋㅋㅋ|1 + 내 말이 틀렸냐 이 씨발새끼야.|1 +저거만지는거 여자냐|0 +착하다 착해일베의 빛이다ㅇㅂ|0 +와꾸는 주관적이긴한데 엑퀴들은 안좋아하더라|0 +대한민국 최고!!!!|0 +너 나좀 볼까|0 +나중에 바이브레이터 사서 보지에 지이이잉~ 할듯|1 +ㅂㅅ이노 ㅋㅋ 수요와 공급. |1 +짱골라새기들 요새 스키여행으로 삿포로 자주오는데시이팔~ 존니더럽게쓰고간다참고로 한국년놈들도 노재팬운동한다더니 스키,온천여행 존나게옴ㅋㅋ|1 +쓰레기통에|0 +엥가이 해라 시발로마|1 + 여기서 조선인마인드가 왜나오냐 미친새끼야|1 +와 씨발 진짜 성형했네.관심있게 봤음 충분히 이상하다고 느낄 부분이였는데...진짜 노짱은 그래도 성형한다고 솔직히 말하고(지 눈썹찔린다고 안검하수..) 했는데이새낀 다 역겹네. 거짓말이 아예 베었어 퉷|1 + 잘사는 동네 일수록 |0 +예전에 140 썩을놈의 새끼 생각나네요.|1 +그래서 일본 백화점 이런 고급 슈퍼도 타겟을 젊은층 보다는 부자 노인으로 공략중 |0 +하네|0 +솔직히 이육대편 딱 하나는 b급 컨텐츠로 재밌게 볼 수 있고 공감을 얻을만했다고 생각하는데 이전/이후로는 씹노잼에 뭔 재미인지 알수가 없음 펭수 자체의 드립력도 노잼이고 컨텐츠도 노잼임|1 +하지말고|0 +저거 일본돈때문이아니고 올린놈이 사기꾼새끼라 저렇게 적어놓은걸로 알고있다.|1 + 그 클럽도 보니깐 축구농구같은 인기스포츠가 아니라 육상 복싱 가라데 등등 다양하게 하더라|0 +근데 이건 새로온 부장말도 들어봐야 되는거라 ㅁㅈㅎ|0 +남자들은 씨를 말려야해!!! 라고 페미들이 말하는거랑 같은논리!!|1 +그냥 너희들끼리라도 해...|0 +저 역시 윤짜장을 지지했었기에 지금은 반성을 합니다.|0 +그래그래 고생했네|0 +와 진짜 다시들어도 피가 거꾸로 솟는다누구때문에 안전하고 평화롭게 살고있는지한번도 생각을 안해봤다는 소린데시발 미친 김치년들|1 +얘 남자다 고추달린 요자임 ㅋㅋ 레이디보이|1 +지이이잉 나가 디지라|0 +노무노무 쪽팔리겠다ㅋㅋㅋ|1 +kill yourself|0 +지랄하고~자빠졌네~|1 +일본이 군대를 가냐??|0 +17억 임대료면 세금을 얼마내냐|0 +와 꼴린다 ㅅㅂ 기사보고 딸쳐야겠노.|1 +애시당초 아무나 음식다먹는데 저걸 가지고 빠는새기들이 개특이한거임 노력안해도 돈을 쓸어 담앗으니 문화가 진짜 미개한 나라임 .힘들게 땀흘려 돈버는사람들 기만하는거지 .. 에혀 |1 +우리나 미쿡이나 그리 다른 것이 많지는 않군요.|0 +친구를보면수준이나온다하던데맞네ㅅㅌㅊ|0 +섹스자지보지|1 + 살아남음,나도 복지정책에 반대하지는 않음|0 +저거 존나 비싸던데 어떤 중딩이 입고왔길래 얼마냐고 물어보니까 70장 줬다고하드라 ㄷㄷㄷㄷㄷ;;; |0 +씨발 노가다꾼새끼 팩트폭행 쳐맞고 부들부들하노? ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +쓉쒱키는 |1 +ㄹㅇㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +일본 에이형 나란데 ㅋㅋ 한국이 실권은 오형 비형이 쥐고있고 에이형은 노예고|0 +정상도 소량 hcg검출되는거아니냐|0 +손흥민 봐몇 주 전까지 자기들끼리 월클이니 뭐니 빨다인성 논란 터지니 뭘 해도 욕먹고 조롱당함선수 한 명에 감정이입해서 물고 빨고 지랄|1 +두달뒤에 쥼.ㅎㅎㅎ|0 +김혜수동생아님?|0 +펭수 하는짓거리가 페미니스트네 씨발 좆만한새끼가|1 +진짜 개좆같음 딱히 세입자 문제가 아니라 모든 상황에서 저럼|1 +뭐래 븅신이 ㅋㅋㅋㅋㅋㅋ 통계가 뜬건데 니 뇌피셜이 아니라 통계라고 ^^|1 +아직도 언론을 모르시네.. 이제야 이런 기사를 낸건 면책성입니다. 즉 국민들에게 우리는 기사 냈다? 이런 의미요.|0 +구경만해야지|0 +못생김|0 +afpk는 따기 어렵지 않아서 많이들 갖고 있다. cfp는 갖고 있는 사람이 별로 없고|0 +니새끼가 투표한다고 와닿는게 있냐??|1 +또 튈려고? ㅋㅋㅋ 5번 빤스런 기록 ㅋㅋ|1 +한두개 빼고 다네 ㅠㅠ|0 +절대시계 주냐?|0 + 닭이이자스민공천할때 대통령 이명박은 간섭안함 근데 김무성이 공천해야할걸 닭은 간섭함 닭잘못 ㅇㅇ|0 +문제는 남자들이 재혼을 해도 딸이 위험에 처하는 건 똑같다아무리 좋은 여자라도 남이 낳은 딸을 자기 딸처럼 사랑하는 건 어렵기 때문에|0 +섯다|0 +어이 ATM 잔말말고 가서 돈이나 벌어와|0 +나는 베충이들이 하도 김성령 김성령 해서 진짜 엄청 괜찮은줄 알앗는데...상속자들 드라마 보니까 그냥 전형적인 예쁜 아줌마던데 그냥.|1 +직업에 귀천은 없다 하지만 그 직업에 종사 하는 새끼들이 그 이미지를 만들지 |0 +조샌징이 더 최악임|1 +농어촌이면서 공부 되게 잘했던거처럼 방송에서 말하더만;;|0 +병원은 자선사업이 아닌데 본인만 의로운일을 하려는거지 그냥 의료봉사나 다니세요|0 +20년은 젊어졌노? 근데 목을 잡아늘인것도 아니고턱은 더 길어지고 다른사람인듯|0 +슬슬 정부 씹고, 대구가 잘해서 극복했다는 니우스 뿌리가 ㅎㅎㅎ|1 +지랄하고 자빠졌네..|1 + 나는 이 아이디 로긴 안 하니까 니 혼자 댓글 달고 지랄 쌈싸먹어라 어차피 안 보임 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +부를 이루면 다음 단계는 명예욕이라는데 그 말이 맞다|0 +중대수학 좋지|0 +어우 난 사더라도 짭팅어 살려고 했는데...ㄷㄷㄷ|0 +정지선 칼 같이 지킨거 보소|0 +육군은 진짜 전투복 븅신같네|1 +김무성은 좌파라고 정치적 성향을 얘기하기도 민망할 만큼 정치색이 없는 양아치다. 동시에 철새이고 박쥐이기도 하지. 그런놈이 어쩌다 박근혜 뒷줄서서 자칭 보수당인 자유한국당에서 거드럭거리고 있는거지. 탄핵주동자도 쳐내지 못하는 자유한국당이 보수 ? 그런 소리 씨부리기 전에 박근혜의 죄좀 찾아내봐라. 3년동안 찾아도 개인의 비리는 하나도 안나오는데 정유라 말 사주라고 삼성에 얘기한거랑 재단 만들때 기업들한테 앵벌이 좀 한걸로 직권남용 ? 그런 논리면 역대 대통령은 물론이고 현역인 문재앙까지 싹다 깜빵에 넣고 무덤 파내서 부관참시해야지. |1 +과이불개시위과의 길게 쓰지마라 어차피 안읽고 신고행이니^^|0 +술을 먹어 만취 상태가 되면 무조건 구류 20일 선고하는 법이 필요하다. 혼자 취해서 남한테 해를 끼치지 얂아도 곤드레 만드레 하면 쳐넣는거지|0 +그래 잘했다 성공해라|0 +이투스 불법 댓글알바 이겨서 몇명 감방보내고|0 +너는 하나만 알고 2는 모르는거 같다|0 +어제 직원식당에서 쏘세지 배식 담당인 씬이 존나 웃겼는데 ㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +스티브잡스같은 혁신가가 왜 팀쿡같은 고리타분한 관리자형 인간을 CEO로 맡겨는지 이해가 간다|0 +가등청정|0 +미통닭새기들과...|1 +근데 팩트임 ㄷㄷㄷㄷㄷ|0 +확률이 없진 않지만 있다면 정말 미비한 수치지; 거의 제로에 가까운 확률일듯.. 아무리 수치적으로 맨시티가 역전할수있는 시나리오가 있다해도 실질적으로 리버풀이 우승맞긴함.|0 +이새끼 위장홍어다 ㅁㅈㅎ줘라이새끼 위장홍어다 ㅁㅈㅎ줘라이새끼 위장홍어다 ㅁㅈㅎ줘라이새끼 위장홍어다 ㅁㅈㅎ줘라이새끼 위장홍어다 ㅁㅈㅎ줘라이새끼 위장홍어다 ㅁㅈㅎ줘라이새끼 위장홍어다 ㅁㅈㅎ줘라이새끼 위장홍어다 ㅁㅈㅎ줘라이새끼 위장홍어다 ㅁㅈㅎ줘라이새끼 위장홍어다 ㅁㅈㅎ줘라이새끼 위장홍어다 ㅁㅈㅎ줘라이새끼 위장홍어다 ㅁㅈㅎ줘라이새끼 위장홍어다 ㅁㅈㅎ줘라이새끼 위장홍어다 ㅁㅈㅎ줘라이새끼 위장홍어다 ㅁㅈㅎ줘라이새끼 위장홍어다 ㅁㅈㅎ줘라이새끼 위장홍어다 ㅁㅈㅎ줘라이새끼 위장홍어다 ㅁㅈㅎ줘라|1 +용암 속에 들어가게 되면 일베손모양 유지해줘라.|0 +옆에서 쓴소리하는 사람이 없으니깐 저러지 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ박근혜도 옆에서 쓴소리하면 보지식 기분나빵! 하면서 다 쳐내니깐 탄핵당한거고|1 +비리도 어마어마 하더만......|0 +다애미뒤진 새끼들이라그래|1 +심각한 골절상 당하면 그 고통과 회복과정은 정말 끔찍하고 아프죠.|0 +홍고나왔냐 씹새야?|1 +담배피냐|0 +보집물 맛보고 싶다|1 +아ㅋㅌㅋㅋ|0 +씹찐따 새끼들아 형이 정리 해준다. 신발 레드윙 바지 lvc 자켓 a2이렇게만 입어라 바로 아다 땐다|1 +08에 땅값이 떨어져서 지금 떨어졌냐고 병신아 ㅋㅋㅋㅋㅋㅋ 아니 시발 08년도에 지구 망했냐? 인생 종쳤어? 08년도에 리먼브라더스 사태가 와서 모든 게임이 끝났나보네 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +고쳤네.. 코.. 입..|0 +좆소는 대기업보다 더하지. |1 +이 한국의 화이트 컬러라고 깝치는 새기들 속마음을 알수 있었던 계기가 된게 이번 사건임내심 속으로는 다 무시하자나 ㅋㅋ 허드렛일 하는 ㅈ병신들이라고 일베나 지금 타사이트만 봐도 맞는말 했다 vs 그래도 경솔했다 두 의견으로 나뉘는데여기서 맞는말 했다 라고 하는 애들은 그냥 생각이 없거나 솔직한 새기들인거고 그래도 경솔했다 라고 말하는 애들이 그래도 나름 생각있거나 겸손한 애들임요직업의 귀천이 없다고 늘 떠들어 대나 대한민국에선 귀천은 있다 라는 걸 보여준거임 한국의 이중성을 여실히 보여준 경우지 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ직업은 필요성에 의해 생긴다 그리고 귀하고 천한건 판단하는건 본능임 다만 입밖에 꺼내서 일하고 있는 종사자들 엿맥인거랑 속으로만 생각하는건 천지차이임|1 + 근데 요즘 그런 여자들 진짜 존나 없다 일단 사내에도 괜찮은 여자들은 이미 다 결혼했거나 남친이 있고|1 +와 씨발 진짜....|1 +병신년들 평소 짤게에서 온갖 직업들 조리돌림하던 새끼들이 이제와서 무슨 성인군자라도 납셨냐?죠센징 마인드 어쩌구 지랄하기 전에 느그들 자신을 먼저 돌아보는건 어떰?ㅉㅉ|1 +오늘가입한사람 아니라면 국게에서 적어도 한두번|0 +김무성 옥새 파동 모르노?|0 +저 아나운서가 형광등백개드립러 아녓던가요?|0 +아니 인덕션은 애플만 하라고|0 +저정도면 찍으면 넘어온다. 내말 믿어라.철벽치는 김치같았으면 저렇게 답장안온다용기가지고 밀어부쳐라!!단 밀어부치다가 철벽으로나오면 미련없이바로 접어라.그게 너에게도 좋고 김치들은오히려 그런모습에 다시 올수도있다.|1 +진쟈 정신이상자들도 돌아다니는거 보면 어이없음 ~ 일상적인 대화가 욕뿐이 없고 ~|1 +닥쳐라좀|1 + 도선사도 그냥 대충 주차만 해주면 끝이네? ㅋ|0 +군대스리가|0 +이게 정상|0 +밑에꺼 난 항상 철망치같은거만 골랏엇음 ㅋㅋ|0 +틀니대표당으로 바꾸라니깐 ;;;|1 +한국 연해주땅 상의도 없이 러시아에 팔아넘긴게 중국임|0 +백수 천지구나...제주도 까지 돈까스 먹으러 간다고하면..부모가 한숨 나오지|0 +엄마 프로필에 그분이 보이는건 나뿐이냐? |0 +어데로 가십니까|0 +아 그냥 군대나 빨리 가지 왜 안 가는 거야? 하여튼 홍어 빨갱이들이란|1 +서울에서 접근성 ㅅㅌㅊ . 서울랜드 사람 많을 때는 바로 옆 복돌이 랜드로—|0 + 틀들은 자꾸 사실과 망상을 구분안함|1 +어떤 동물이랑 했노|0 + 일제의 개입이 없었다면 쇄국정책이나 계속하면서 미개하게 살다가 러시아 같은 제국주의 국가들에게 훨씬 비참한 모습으로 짓밟혔을 것임.|0 +돈이 싸움원인 90프로|0 + 불타는 연옥이 널 기다린다 이기야. |0 +게이야..집까지 한시간 가량 소리지르면서 우는거는 민폐아니야..우는 모습도 안귀여울꺼 아니야|1 +저런애들중에 나중에 기술사따면 인생 개 씹 ㅆㅌㅊ임|0 +저 년은 결혼 해도 지가 아니다 싶으면 뭐든지 멋대로 할 년임|1 + 일손이 부족한 부서에 공무원을 채용하는 것은 문제가 없는데|0 +그 틀딱에게는 진짜 삼성이 좋다 |1 +5년만 존버하면 1억찍노 ㅋㅋ|0 +아 오늘 다스뵈이다 기대되네요 ㅋㅋㅋ|0 +댓글수 354|0 +사실은 그게 맞는건게|0 + "이 오빠가 지금은 이래도 왕년에..."|0 +공부성실히못할것같으면 기술배우라는 말아니냐?? 어떻게 말했는지 몰라도 아주 틀린말은 아닌데 오히려 감사해야함|0 +받고 따블로가 ㅋㅋ|0 +18,862 동의합니다|0 +숏패딩은 눕시 디자인미만잡|0 +기부라..한국인들이 진심이 있었을까 ㅋㅋ|0 +언제나 그렇듯, 중앙일보의 변함없는 일본 사랑.|0 +고추 잘라삐자|1 + https://news.naver.com/main/read.nhn?mode=LSD&mid=sec&sid1=102&oid=014&aid=0004357560|0 +좆문가 o좃문가 x|1 +싸게먹힌다기보다 그게 원래대로|0 +나도 약속어긴다에 내 쌍방울건다..|1 +어떻게 기억함??나는 싸이트 마다 비번 다 다르게 해놓았더니 기억이 안나고 비번찾기 용 메일도 다르다고 나와서 계정 새로만들려고 했더니 이미 있는 사람이라고 안되고|0 +아 정말 tv 이쁘고 귀여운애들만 나왔으면 좋겠다. 쉰것들 쫌!!!|1 +질문 하나만 해도 괜찮아??|0 +닭끄네 최초로 탄핵당한 멍청하고 병신같은 개보지^^|1 +1사단 12연대 3대대 9중대 3소대 영원한 불사조야 전진|0 +저 여자들 중에 제대로 정치할 사람이 얼마나 있을지?????|1 +치부책이 어디잏더라|0 +꼭 좋은날 올거예요..힘내세요..ㅜ.ㅜ|0 +일단 희생시키고 난 다음 거룩한 희생으로 미화시켜주면 다들 이해 된다고....|0 +시바라 뒤질래 |1 +아 알겠다 그럼 조선소나 숙노로 ㄱㄱ|0 +하하 가자마자 추격전 큰거 세개 터지고 (돈가방, 여드름, 꼬리잡기) 가요제 2회부터 본격적으로 듀엣 하고 , 장기프로젝트로 에어로빅도 하곸ㅋ 졸라 커질만한거 많이 했음|1 +ㅇㅇ 나도 스팀가드 모바일로오게함 이메일 허벌이라 ㅋㅋㅋ|0 +오 ㄱㅊ노?? 박 하나 심어놓은다음에 존나패는거|1 +피부 좋아짐 |0 +요즘 대세는 버스터즈 예서라고 하더라 ㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷㄷ|0 + 예수님은 그 강도보고 '넌 지은 죄가 너무 많고 회개도 안하고 양을 바치지도 않았으니 안됨 너 지옥' 이라고 하지 않으시고|0 +틀|0 +사운드 오브 뮤직은 열번 넘게 봤네요..|0 +떠나고 나면 후회와 아쉬움 그리고 공허함만이 남음..|0 +앜ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ건지 카와이|0 +지랄하겠네 영안실써비스가 뭐같다고|1 +신천지교인은 사비로 검사받게 해야합니다|0 +사실 별거 없지 잘은 해 밥벌이|0 +원래 김여사의 운전법은 상상을 초월하기에 |0 +그런건 어딜 봐야 아노|0 + 중고신입인 경우 34~35살까지도 봤다.|0 +이미 눈이|0 +개인정보 제공 이유가 그거였노 ㅋㅋㅋ|0 +사람을 얼굴로 평ㄱ|0 +저럴거면 집에서 하지 |0 +ㅋㅋㅋㅋ 이탈리아래|0 +븅신새끼는 ㅁㅈㅎ 야 |1 +나도 유인나 즐겁게할수있는법 ㅇㄷ|0 +진상 세입자들이랑 명도 분쟁 겪으면서 별별 놈들 다봄.도망치듯 집행전에 나갈때 백시멘트 변기에 쳐붓고 내리고폼우레탄 하수구에 뿌리고, 우수관에 파이프 박아넣는 놈 등등.당장에는 문제 안생겨도 서서히 건물 골병드는 짓 하고 튄놈도 몇놈 봄.|1 +맞음 이새끼 ㅈㄴ 죽여놔도 시간 지나서 cs쳐먹고오면 존나쎔|1 +생각짧고 멍청한 애들이라..|1 +뭔가 대단히 착각하고 자빠졌는데 병원에서 사람이 부자인지 가난뱅이인지를 안따지고 치료해주는건 복지의 개념이지 그게 자본주의와 사회주의를 나누는 척도는 되지못해.자본주의라고 돈없으면 다 죽이고 병신같이 사는 설국열차만 생각하는 병신같은 좌파마인드좀버려.|1 +응 안사|0 +착취 박이라고 바꿔라...머 홍어만 채용하나...ㅎ|1 +힘내라 대구,|0 +엣지 제발 버려라|0 +그럼 표창원한테는 왜 열들감을 안느껴?|0 +짤품번 DDH-971|0 +ㄹㅇ 역겨운 새끼였는데 안봐서 살거같음|1 +보빨러들은 당해도 돼.결혼하면 세금 몇푼 아끼겠다고 공동명의하는 흑우없제??결혼하면 재산을 무조건 5:5로 나누는 거 아니다.결혼기간, 기여도, 겷혼전재산 다 따져서 나누는거다.근데 대책없이 그냥 공동명의하면 무조건 5:5로 나뉘게 됨. 남자가 돈 더 많이 해가도 똑같이 5:5로 쳐나뉜다.공동명의해서 아끼는 세금은 10억이상 자산가들에게나 해당되니까. 개뿔도 없으면서 세금 아끼자고 공동명의하는 년들 있으면 이혼해라.|1 +새로운 바이러스가 들어오면 사이토카인이라는 면역세포가 우리몸을 방어하는뎨젊은 사람들은 다소 활발하게 세포들이 움직여서 정상세포까지 공격하는 상황이 나오면서열이 심하게나고 몸에 있는 장기들이 망가지는 안타까운 상황이며아직 치료제가 개발되지 않아 더더욱 환자 본인과의 싸움..|0 +주식에 투자할 때는 자신의 판단으로 하면서 폭락을 하면 정부 탓을 하는 것이 대한민국의 현실이지요.|0 +글쓴놈 내일 자살했으면 좋겠다^^|1 +나 때는 SSKK 아니면 살기 힘들었지 난 5년 하고 추노해서 이민함. 이민 전에는 모른다. SSKK 얼마나 미개한지... |1 +난 거울 안보고 하는데 나중에 다 밀고 한 번 보는거지내 설명이 모자르면 유투브에 셀프컷 쳐봐라. 그럼 걔들이 자세히 알려줄거임|0 +저짝 동네가 아시아의 멕시코지|0 +귀엽다 ㄹㅇ 감시하네 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +가버렷!~~~~|0 +지금은 200넘겼을듯|0 +저거 지금 아니다 어제 기사고 |0 +못사는놈들은 원래 염치라는게 없음그래서 더 못사는거고길같은데 아무데나 가래침뱉고 오줌싸고 공공질서같은거 하나도 안지키면서 정작 본인들 손해난다 싶으면 바로 피해자코스프레함|1 +리더의 말은 그럼 "박쥐도 없는데 코로나 그거 잡기 힘들답니꽈? "이래야되나 보지?|0 +162zz|0 +저렇게 되면 이제 검사도 경찰서 붙잡혀 가서 짭새한테 욕 처먹어 가먼서 조사 받아야 된다 ㅋㅋ 검사 나으리 좃밥다됐노|1 +아니 ㅆㅂ 그런 "카더라"가 넘쳐난다고, 그거에 대해서 혹시 댓글게이는 뭔가 아는거 있냐? 진짜냐? 하고 물어본 새끼한테 "근거는?" 이 지랄은 또 뭐야?|1 +좋겠네요|0 +가만히있던 CU만 봉변당했네 ㅋㅋㅋㅋㅋ|0 +공부 잘했다고 다 사회에 필요한인재는 아닌거같아요...|0 +ㅂㄷㅂㄷ하는구나 ㅋㅋㅋ|1 +공지영 트리플파더랑 김제동 모쏠 들어가면 완벽한데 ㄹㅇ|0 +나도 어릴 때는 때를 덜 타서, "직업에는 귀천이 없다"라고 생각을 했었는데개 핫바지 일을 하다 보니까 직업에는 귀천이 있다는게 뼈저리게 느껴지더라ㅋㅋㅋ그래서 지금 일하는 곳에서 사명감, 소속감 하나도 안 느끼고 다른 동료들도 마찬가지로, 시간 때우다가 퇴근해야겠다는 마음이 큼|0 +대구에 사는 젊은 사람들은 한 번쯤 생각 해봐야 한다..|0 +공부 잘하는 애들이 의사 하려는 이유가 사명감이 클까, 이윤추구가 클까?|0 +맞는말인데 통레발은 이제 믿기가 힘듦,,|0 +이게 잘사는 동네와 못사는 동네의 차이냐?잘사는 동네의 특징이지중졸새끼야|1 +애초에 방송 자체가 작정하고 편협하게 만든 쓰레기 다큐비타민 혈중농도 측정하는데 몇시간이면 다 빠져나가는 수용성비타민만 측정해서 뭐하노?? 비타민d 수치가 존나 중요하고 영양제 먹고안먹고 판이하게 차이가 나는 영역인데 그건 쏙 빼버림 ㅋㅋ 아니나다를까 띵승권 등판, 엉터리 논문 코펜하겐 쇼크 인용 ㅋㅋ 좆병신새끼들 언제까지 저런 짓 할려나. 영양제 효과와 사례들과 메커니즘을 깔쌈하게 정리해서 팩폭하는 다큐는 대체 언제쯤 나올런지?? 어제도 그거 기대하면서 봤는데 아니나다를까 다 아는 병신같은 무용론만 주장하고 끝남|1 +일부러 누런색 안사고 회색 샀는데 회색이 더 많이 보이더라 ㅅㅂ|1 +아무리 그래도 그렇치 임마. 엄마한데 오_나홀 이모티콘을 보내면 어떻하냐? 임마.|1 +이 나라는 사농공상 인식 철폐부터 해야한다. 일본 정부는 옛날부터 사농공상 인식 철폐를 위해 많은 노력을 했다. 휴....|0 +사유리같은 느낌이든다...|0 +수갑을 누가 저렇게 줄 넉넉하게 해서 채우노|0 +개보다 못한건가ㅋ|1 +군살녀, 루저녀 둘 다 불쌍하다말실수를 한 건 맞는데 사람 죽인 고유정 수준으로 욕먹고 사희에서 매장당함|0 +네 다음 공현주|0 +남들이 보기엔 어떨지 모르겠어|0 +도투락월드는 남아있냐|0 +관장좀 하고나서 씨부려 아이참 냄새나|1 +15억 제한걸면 밑에껄로 투기 한다고 일베애들도 그리 말하더만 이 정신병자 정권은 시장을 거슬리려고 환장을 했어|1 +안타!헛소리하지말고 사라져주실래요?|0 +천주교나 불교를 봐라..알아서 집회안하쟌아..사람이 우선이지 니네 돈벌이가 중요하냐?|1 +이글 보고 우리나라 생각하니 다시보니 선녀같다!|0 + 이건 지가 써놓고도 이해를 못하나|0 +빵에서 후장 파열로 뒤질놈임 ㅋㅋㅋㅋㅋ|1 +오~레오레오~|0 +푸하하하하 고맙다 미친것들아 이렇게라도 웃게 만들어줘서|1 +ㅊㅊㅊㅊ|0 +영자 뭐하냐 현거래 불법이다. 23렙 전부 영정먹여라.|0 +도요타방식 |0 +서성한 인문상경(O)|0 +응|0 +김희철/한가인/이연희 지린다 ㄷㄷ |0 +유재석 노래 음식점에서 맘충들이 흥얼거리고 존나 따라부르던데 가만히 들어보니까 잘부르는 것 같기도 하고...|1 +메가도스한거 맞음?그리고 막어보면 부작용 잇는거 보면 효과도 잇다는 반증이고..그냥 먹은양이 적어서 저런결과인걸꺼다. |0 +일베 노가더, 6,7등급 병신들 풀발기 하고 있음 ㅋㅋㅋ|1 +이때다 싶어서 편피노들 활개치고 다니네 ㅋㅋ|1 +쓰레기들 퉤|1 +방어자세잡고 안넘어졌긴한데...애매하네요|0 +뭔소리야 단순히 일본이그렇게 잘났으면 일본이 다 1등해야지 삼성한테 다지는데 뭐가 일본이 잘나가|0 +틀린말은 아님 근데 유튜브하는새끼면 이미지 관리해야하는게 맞음 ㅋㅋㅋ 아가리 잘못 놀리면 그케되는거임맞는말도 쥬변환경 봐가면서 해야하는거임 쟤가 분위기 파악 못한건 지 잘못임사석에서나 그런얘기하지 ㅋㅋ|1 +노팩트ㅁㅈㅎ|0 +아니 그 공부를 해야한다니까 갖다 용접만하면 되는거이 아니고 철하고 알루미늄하고 졸라게 용접해봐야 붙질 않아|1 +유희석 ㅋ|0 +월수익 36억이랑 편집자 월급이랑 무 슨상 관이냐 ㅋㅋㅋ더 버니까 더 내놓으라는거? ㅋㅋ 전라도 마인드 보소 ㅋㅋㅋ|0 +정의롭고 깨어있는 이 시대의 행동하는 양심이시다. 욕하지마라.|0 +저는 껌 안 씹습니다.|0 +Byungshin Jesse요?|0 +팝콘을 준비하자 ㅋㅋㅋㅋㅋㅋ|0 +둘 다|0 +이석기경기 연합을 여론조사 조작해서 해산시켰다며 빨갱이 옹호하는거야?|1 +택배 상하차|0 +보고팠습니다.|0 +메뉴가 둘이 왜 겹치냐?|0 +자지 색깔이 검정인것이 말로만 듣던 전설의 오골개냐?|0 +수리할일이 일년에 몇번이나 있겠냐|0 +극정말그래이 청정원이 이때아이가|0 +서울대보낸 과외생만 몆명인데 호ㆍ후|0 +차차차~|0 +빅파이 누군데|0 +바닥에 용접시킴|0 +위장이혼으로 부정수급받는거 불법 맞아지금 댓글 보니깐 법이 어쩌니 영리하니 뭐니 하는데그게 아니고 공무원들이 일을 제대로 못 해서 적발이 안된거임혹시 아는 사례 있으면 신고해라http://www.mohw.go.kr/react/al/sal0301vw.jsp?PAR_MENU_ID=04&MENU_ID=0403&page=114&CONT_SEQ=333086포상금 받을 수도 있음|0 +저래도 자한당 안찍어 ㅋㅋㅋㅋ|0 +그새끼 칼침 놔줘라 |1 +난 37살낀지공부햇다|0 +처맞으면 날아다님ㅋㅋ 완벽한 아웃파이터였지 그새끼가 챔피언 한번 했을걸?|1 +젤 바보 같은 프라이드팬들의 착각임4점 니 혹은 스템핑은 둘 다 상위에서 아래로 공격하는거고당연히 둘 모두 상위포지션 점령율이 높은 레슬러에게 유리한 규정임|0 +재앙이가 위에 나오는 움짤보고|1 +앜 시발 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +와ㄷㄷㄷㄷㄷㄷㄷㄷ할배 트라우마 좆되겠네ㄷㄷㄷㄷ곱게 바퀴약 쳐먹고 뒤지지 저게 뭐야|1 +명예의 전당에 올라갈 글이네요^^|0 +팔려다니는 북한 창녀나 자발적으로 몸팔러 다니는 한국 창녀나...걍 그게 그거 아니냐?|1 +다 아는 게임들이구만|0 +용접유투버 용접이 모두 계산일고 하는데 솔직히 그건 산수지.|0 +자한당은 끝까지 정신못차리노... 저지랄 하니까 보수대통합이 안되지... 민주당이 하는 병신짓은 다 골라하네ㅋㅋㅋ|1 +그래 좀비월드 되어 다 주거라|0 +니놈은 노숙자와 범죄자들한테 윤간당한 니 에미가 싸지른 좆물쓰레기고ㅋㅋㅋ ^,.^|1 +2. 즉설연설? 시킨거고 북 찬양하는 얘기만하니 선전용으로 데려다니며 써먹었다|0 +그럼 모두가 공평하게 평등하게 치료받아야합니꽈? 허허허|0 +남의 차라 부러워서|0 +벽면에 툭 튀어나온 콘센트... 일반 아파트는 아닌거같고 허름한 주택이나 창고 컨테이너인거 같다|0 +아는 누나 부산대컴공인데 일본에서 스카웃받았다더라|0 +색시한남자|0 +그거|0 +문재인은 사과도 안하잖아..|0 + 3·1 만세 운동 진압 목적으로 결성되었으며, 전라북도에서 처음 조직되어 전국으로 확대되었으나 자제단 만큼 세를 확장하지는 못했다. 자제단 만큼 실적을 올리지는 못하였으나 각지에 설립되어 3·1 만세 운동 참여자를 설득, 귀가조치 하거나, 불응하는 자는 지부와 본부를 통해 경찰에 신고하였다. 다른 이름은 자생단이다.[1]|0 +ㅋㅋㅋ그러네|0 +판이 점점 커지노 ㅋㅋ걍 얼마 안가고 묻힐 줄 알았는데 크루들이 들고 일어났네|1 +아 이걸ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +다 싹 뒈지길|1 + "우릴(대구 경북) 싫어하는 애들은 다 전라도야!!! 비전라도애들은 우릴 안싫어해!!! 빼애애액 네다홍!!"|0 +나중에 자식들이 다 알아볼텐데...|0 +반대가 꾸준히 있네요.|0 +ㅋㅋㅋ 내가 넷마블 장기에서 90승10패인데 ㅋㅋ어찌했냐면 장기도사라는 인공지능프로그램이 있는데 이거 켜놓고 그래로 따라하는거임 존나 나온지오래된 프로그램인데 지는경기가 없음 ㅋㅋ |1 +요즘 신종사기가 이거임틀딱들이 40대 아줌매미 에게결혼하면 10억대 아파트 명의준다고 꼬심알고보면 빚이 10억이상임근데 좋다고 결혼해서 틀니노후 뒤치닥거리함물론 틀니뒤지면 이자못내서 나앉음노후대책 ㅆㅅㅌㅊ|1 +사이비도 명예가있긴 있냐?|0 +그냥 저당시 6천이면 아파트 한채 샀고|0 +일해도 찜고딩때 동창들 결혼하더니살 쪄서 아저씨 됐더라13군번임|0 +이젠 시발 특문은 내가 그냥 이해하겠는데 영대문자 넣어서 비번 완성하라는곳은 개씹욕나옴.|1 +일베 실검도 1위를 못했는데 뭔 수고가 많대 ㅋㅋ|0 +신라가 합리적이네 한국같은 약소국은 외교술로 꿀빨아야함|0 +중대 수학과면 자대 중에서도 ㄹㅇ 하위권 아님?아마 저 사람도 수능땐 수리가 1등급 못받았거나 다른 과목 낮아서점수 맞춰서 간거 아닐까?근데 뭐 따지고보면 중요한 건 가르치는 기술이 좋으면 되는거니까 노의미|0 + 무역수지는 1조 1877억엔 흑자고.|0 +지진으로 태평양 한가운데까지 떠밀려가서 혼자 고립되어야지 ㅋㅋㅋㅋ|0 +그렇죠 절대적으로 자국의 이익이 최선인 나라입니다. 이후 상황에 한국에 기댈게 많다는 증거지요.|0 +나중에 중국에 북한여자 사러가야겠다 이쁜애들 2~3명 골라서 육변기로 쓰다가 한국에 풀어주면 될거갇은데|1 +우병우 짱..완전 존경합니다~^^|0 +그래서 경찰부부는 바람 많이 난다|0 +형 묘자리는 알아봤수?|0 +ㅂㅅ새끼 랩업에 목숨건 새끼네 ㅈㅈㅂ만 존나게 올리네|1 +할말있냐?|0 +이쁘다|0 +용접 타일 도배 이딴게 뭔 기술이야 병신들아..머리자르는 깍새도 기술이냐?|1 +고기자영업자다저거 사장이 미친년이 분명하다|1 +보빨남은 더 당해야 한다.|1 +지방 깡촌이라도 최소 3-4억은 줘야지|0 +베댓들이 장문이네 ㅋㅋ|0 +ㄴㅈ|0 +거 무슨영화지?|0 +시시각각 브리핑하며 투명하게 진행시켜야 함.|0 +일본 명예시민권있는거지?|0 +용접공이 뭔 산업역군이노 ? 솔직히 공부 못하고 할 거 없으니가 직업훈련소 같은데가서 그거라도 배워서 입에 풀칠하는거지 조선소에서 조금만 일해봐도 용접공새끼들이 얼마나 개새끼들인지 알게되는데 범죄자새끼들도 존나 많고 교도소에서 용접기술 배워와가지고|1 +올해 안에 꼭 자살해라 존못새끼야 ㅋㅋ|1 +곤니치와봉주르 국내업체인가요?? 이쁘네여 디자인|0 +별거 아니긴, 전국 용접 하는 사람들 순식간에 대가리 나쁘고, 공부 못해서 어쩔 수 없이 용접하는 놈들로 만들었는데|0 +넌 나의 세상 넌 나의 빛 oh my heart|0 +니가 말하는 공부는 진짜 연구 목적이고|0 +ㅇ|0 +캬악 퉤 |1 +완전 오태식이네 다 뺏기니까 다 때려부수려 함 ㅋㅋㅋㅋ|0 +어휴 또 가세연... 가세연이 물엇다니 우리 예지 응원해야겟노 |0 +글쓴놈 요점 파악 안 되지?니가 제일 똑똑한 거 같지?가세연에서도 김세의가 글쓴놈과 똑같이 말하더라그랬더니 강용석이 요점은 그게 아니고 7등급 밖에 못했으니까 용접하라는 식으로 비아냥하면서 징징거렸다는게 문제라는 거임라고 말하니까 김세의 찍소리도 못함|1 +신박한 미친년이네 ㅋㅋ|1 + Gimbap and norimaki now refer to distinct dishes in Japan and Korea: the former called kimupapu (キムパプ) in Japanese and the latter called gimchobap (김초밥; "gim sushi") or norimaki (노리마키) in Korean. Gimbap usually contains more ingredients and is seasoned with sesame oil, while norimaki is rolled with fewer ingredients and is seasoned with rice vinegar.|0 +나도모르는데 그냥 말해본거야|0 +우덜이 추구하는 세상이 이런거지 현존 악마들|0 + 너무 팩트로 쳐맞아서 대가리가 기능 못하나?|1 + 당장 댓글만 봐도 김씨, 노가다, 맞는 말인데 뭐가 문제 라는 내용을 보는 게 힘든 것도 아니고|0 + 내 동기중에 팀장다는 새끼는 산술적으로 5% 밖에 안되 병신새끼야.|1 + 일본에서도 어린 여자아이돌 잘나가는 애한테 팬싸인회가서 칼로 찌르려던 남자도 있었음|0 +폐쇠 ㅁㅈㅎ|0 +홍은동, 홍제동도 요새 전반적으로 집값 많이 올랐다.|0 +자 첫 스타트 끊어졌고 ㅋㅋ|0 +싸강 지겨웠는데 걍 1학기는 싸강 게속 들어야겠다|0 + 특히 인터넷쇼핑 좆같은문자랑 번호 입력할 필요없이 지문한번 갖다대면 되는게 좋다.|1 +근데 부지런한놈이 게으른놈보단 낫지|0 +절에서 키우는개 아님?티비에나온개 같다 게이야|1 +보기힘든 차라 찍어서 올린겁니다|0 +일본 혐한들은 공석에서도 말 안가리는거 같은데|1 +9급도 혼자 공부해서 못붙어서 강의듣고있음 아이큐 병신 맞지 병신은 맞는데열심히 안살았다고 하는건 말이 심하지그래도 살겠다고 시험 보러오는정도면 지구인구 기준으로 ㅆㅅㅌㅊ 인성인거다 이걸 사회가 너무 당연히 여긴다 ㅋㅋ한국도 미국처럼 범죄자 드글드글 해봐야 정신차리지 내볼때 한국에서 밤에 맘놓고 돌아다니는거 사라질거다|1 +나도 따라서 ㅁㅈㅎ|0 +그래도 20대는 말도 안되는 소리하는 순간 거름|0 +용접은 ㅎㅌㅊ 직업.7등급은 ㅆㅎㅌㅊ 빡대가리임.ㅎㅌㅊ들과 ㅆㅎㅌㅊ들끼리 싸움을 붙인 셈이니 그 강사가 잘못했다고 본다이기야|1 +그거슨 문과 기준|0 +설마 그러겠냐.. 비슷한나이대의 동명이인들 찾아봤다.|0 +저새끼 정권바뀌면 반드시 색출해서 사형시켜야함. 아주 유명한 빨갱이 새끼임 ㅋ|1 +༼◉_◉ ༽ 형.. 그러다 쥬지터진다..|1 +벌레네 ㅋㅋㅋ|1 +절대 쉽게 성공할수업다|0 +고양이는 미련이 없는것같아서 부러움가져가면 가져가는거고 신경도 안쓰네|0 +글쓴이가 저 시기 이란은 마치 지금보다 잘살았다,리즈시절이었다,자유와 인권이 더 ㅅㅌㅊ였다는 잘못된 내용을 실으니까 정정해주는거임.잘 모르는 일게이들은 잘못된정보에 넘어갈거아니겟냐.지금 이 글이 니가 말하는 그 상식을 왜곡하는 내용이다|1 +쟤는 지도 왜 그러는지 모른거고|1 +한국은 엘리트형 스포츠고 |0 + 공무원은 기여금 존나 내는데 받는게 그 꼴인거야|1 +인프라 쪽으로 가면 더 심각해지방이 베트남보다 전기,수도 보급률 낮고 태국보다 PC보급률이 낮음.아직 가스가 전혀 보급안된 지자체가 33곳인데, 전부 지방임.경북 사람의 10%는 아직 집에 전기도 보급되지않았다는 통계결과도잇음|0 +사업은 뭐하냐? 회사생활하다가 사업하는거 쉽지않을텐데..|0 +그냥 주인있는 개인데 혼자 산책 나갔다가 저녁때쯤 다시 집으로 돌아갈 듯. |0 +ㅈㅈ위쪽 뼈에닿을때까지 눌러서 재는거라던데|1 +안전거리 지켰으면 안 박았어|0 +세월호 단식투쟁할때 앞장서 폭식투쟁 진두지휘한 의협 회장 말하시는건가?|0 +개미가 망하는게 그래서인것이고|0 +키야 역갤 식민지 다된 일베 ㅋ|1 +ㅇㅇ 노량진만 가봐도 노력과 실력은 반드시 비례하지 않는다는걸 알수 있음.|0 +조선시대 이씨왕조 = 북빨갱이 김씨왕조|1 +국민이 부유한거 맞음다만 진짜 국민은 소수이고 대부분이 외노자인거지|0 + 대답좀해봐 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +칭찬인데 왜욕함|0 +화이어뱃이네|0 +아아 감사합니다!|0 +지랄염병하고 있네|1 +귀여워서 편들어주려고 했는데 저건 아니지 실수 했으면 깔끔하게 인정해야 어쨌든 말실수 한 건 맞으니까아님 독설가 컨셉으로 바꾸던가|0 +아주 씨팔년들 입만벌렸다하면 구라가 자동으로나와 김구라|1 +저놈이 할소리는 아닌데...|0 +레고로 만든것처럼 생겼노|0 +중국이 축구만 못하는 이유가 뭐냐?|0 +네 다음 광치(광화문 치킨게이라는 뜻)|1 +페페 저만큼 화난거 첨봐|0 +살기 바쁜 인간들은 일베 하고 싶어도 못 한다. 지금 할 일 없는 인간들이나 일베 하지 |0 +월급도 안주고 노예처럼 부려먹는데 그게 합당하단 거냐.완전 홍어마인드네|1 + 이 경황을 보자 그 자리에 있던 조선인 전부가 폭행의 태도를 드러내어 그 일부는 나무편이나 걸상 등으로 반항하여 왔기 때문에, 즉시 나와 병졸에게 사격을 명하여 거의 대부분을 사살하는 데에 이르렀다. 이 혼란 중에 서측의 가까운 집에서 불을 발생시켜, 폭풍으로 인하여 즉시 교회당에 옮겨붙어 드디어 20여가구가 소실되었다..후략..."|0 +ㄴ ㄴ 사람없을때 몰래가져가서 어떻게든 팔려고 궁리중|0 +범죄자들이 큰소리치는 개같은나라|1 +야박하고 인색한년들 존나많음 ㅋㅋㅋ 질투심도 강하고 |1 +지랄하네 저때도 ㅈ망이라고 그랬구만 내 가입일 보고 지껄여라 고딩과 20대 청춘을 일베로 보낸 사람이다|1 +축구할때는 그렇게 한국 한국 하더니....|0 +캐나다 소설이 원작.서양권 전체에서 수시로 드라마 tv 영화 연극 등으로 만들어질정도로 이미 인기 ㅆㅅㅌㅊ그래서 일본이 이걸 눈여겨보고 애니화한거어제 25렙 국회의원허경영 이 개병신새끼가 빨강머리앤은 이전엔 듣보잡인지도엿는데일본덕에 재발굴되엇다는 개쓰레기같은 주장펼친 글이 일베왔더라|1 +이제 페북관리하는 여경들한테 굽신거리지 않으면 바로 깜빵 끌려가는 세상에 왔다 이기야|0 +저거 궁댕이근육단련이야 헬알못 ㅁㅈㅎ|1 +안동에 신촌식당 두개나있는데 뭐하로 청송까지가냐|0 +갑자기 유격 8번 하시노 ㅋㅋㅋ|0 +난 느금마가|1 + 7등급 생키들은 계속 7등급인거야 왜냐면 자기가 뭘 모르는지 모르거든 |1 +그리고 키는 오히려 작을수록 좋음170대 이상부터는 마이너스|0 +정의는 이기지요 힘을내요 바베크|0 + 당신이 오명규 사장이오?|0 +ㅋㅋ 어플로 ㅈㄱ해본 김치녀들 겁나 많음|1 +저딴 생각을 하지?|1 +에휴.. |0 +저걸 시장새끼가 왜 지껄이냐고 ㅋㅋㅋㅋㅋ |1 +쪽빠리는 답없음|1 +상황봐가면서 써먹으면 정치만한게 없음 가만히 지켜보고있으면 그사람이 어떤성향인지 파악할수있음 코드에맞는사람인지 아닌지 맞는 사람이면 오히려 정치얘기 이야기 소재로 삼으면 더 가까워질수있는 계기 |0 +미친늠들.|1 +저 선생말이 팩트이긴함7등급이면 진짜 공부 포기하고 용접 가는게 맞는데문제는 공부못하는애들은 용접이나 해라~이런식으로 말하는게 문제아니냐|0 +이런거 올리지마 ㅠㅠ 치킨 뜯고있는뎅|0 +적어도 페미인지 아닌지는 봐야한다고 생각한다.|0 +근데 진짜 몰라서 그러는데 로스터 업뎃하면 뭐를 할 수있는건가요??|0 +O ㅁO!|0 +도미노피자 근처 있는거 맞지?|0 +강력한 부동산정책 계속 내놓으면 나중에는 부동산국유화 나오겠네.지금 시행하지 토지국유화 주장한 애미추도 법무장관이고,,국회도 문재앙개때들로 장악했고 우리법연구회로 대법원 채웠으니..이러면서 부동산투기는 자기들이 다하고.. 이젠 단물 다 빨아먹었나? 갑자기 강력한 규제한다고 날뛰니..|1 +자칭 명예백인이라고 주장하는새끼들인데 뭐 ㅋㅋㅋㅋㅋ|1 +걸게로|0 +남편분은 빨리 뵙는거 원치 않으실겁니다. 아드님과 더 행복하게 사세요~|0 +내 생각에 일베충들이 이승우 싫어하는건학교다닐때 이승우처럼 생긴애들한테 괴롭힘 당한 탓인거같음사실 일베충들이야 이승우 죽어라 까대지만이승우 와꾸는 현실적으로 찐따 광오후 일게이들 괴롭히던 일진상임사진에서도 키는 작아도 와꾸 때문인지 후달려 보이는 느낌은 없음인터넷 커뮤니티 오래하는애들 99%가 현실에서 좀 동떨어진 찐따들이라서 그런가학창시절, 혹은 현 사회에서 받았던 열등감과 괴로움을 인터넷 뒤에 숨어서 이승우한테 해소하려는 느낌이 강함불쌍한 일베충들...사실 광오후짤만 봤을땐 그 사이에 이승우 들어가면 누가봐도 찐따들 괴롭히는 일진느낌임ㅋㅋㅋㅋㅋ|1 +서울대부심 존나 있어서 주예지랑 생각이 별 다르지 않을 듯|1 +야 진짜 축구만 잘해라 제발|0 +좆무원 새끼들 다 족치고 짤랴야댐|1 +야당이 견제한 적이 한번이라도 있었냐??|0 +대륙의 유전자?|0 +https://m.ilbe.com/view/11205283662|0 +운전안하는 티 좀 내지마|0 +외계인이 없다는 쪽은 개신교가 압도적으로 많습니다.|0 +스팀으로 사출기 만들면 꽤 많은 공간을 차치하고 그공간만큼 더 적재 공간을 늘려야 함|0 +이런 세상이 다있나 싶다|0 +관습으로 일할거면 경찰 왜 뽑누|0 +퇴직자 단체로 소송걸면 회사 ㅈ망함|0 +처음에는 호기심을 자극하고|0 +혼내줬내 잘했다 ㅇㅂ|0 +군대랑 똑같은데?|0 +뭘좀 아노.|0 +딱 봐도 우회전 같이 보이구먼|0 +썅년지꼴리는대로 마리오네트질 할 년임|1 +해결의 의지가 안보이는 일본이군요 ㅋ|0 +두번째 한개의 증명서 오픈하면서 기|0 +아주 지랄싼다 딸 사춘기오면 애미애비 보다 친구랑만 놀음 ㅋㅋ|1 +퇴직급이 아니라 공제금이라니까 |0 +존나 구라치고앉았노이기야 ㅋㅋ 세월호 타러가라이기야 돈 더많이준다|1 +백두산도 질 수 없뜸 터져라|0 + 이유 파악도 못하고|0 +독일이라 하면 되지 바이마르 이러고 있네|0 +갑옷은 일본갑옷 군복은 나치 독일군 장교복|0 +재밌게 건강하게 오래오래사세요~~|0 +솔직히 얘 왜 욕먹는지 전혀 이해안됨진지빨면서 공부 못하면 기술 배워서 공장이나 가라 이런식도 아니고 그냥 농담하듯이 드립 날린걸 쿵쾅이들이 단체로 마녀사냥 하듯이 너무 공격하는거같음영상보면 별거 아닌데 기사 내용들이 너무 자극적임 직업비하!! 이런식으로 써대니깐 그냥 암것도 모르는 사람들은 거기에 선동 당하는듯|1 +맞습니다 맞고요 |0 +우리가 평소에 일반적인 질환들은 건강보험이 어느정도 다 커버를 해주기때문에|0 +생각으로 끝내라. 이때다 라고 확신해놓고 안오면 무신론자들은 '또 지랄하네 ㅋㅋㅋ' 하면서 더더욱 안 믿게 되겠지?|1 +도쿄시 1년 예산 = 대한민국 전체 예산|0 +갓병우 ㅇㅂ|0 +잭슨 진짜 존잘이다|0 +진보팔이 기회주의자.|1 +기술직 천시가 조선을 패망으로 이끌었는데 그놈의 또라이같은 사상이 아직도 남았어 시발이 조선이란 나라는 DNA 레벨에서 영원히 안될거다|1 +곧 제삿밥 얻어먹을 나이라 그런가봄|1 +ㅂㅅ 용접은 몸상하잖아 동사무소 공무원은 월급 루팡이다 인원도 많으면 더 꿀이지 용접하다 장애인 된 사람이 한 둘이냐 사회적 인식도 ㅆㅎㅌㅊ|1 +탄력있고 빵빵한 여승무원의 엉덩이가 나의 우람한 어깨 근육에 비벼질 때의 그 느낌여승무원도 그걸 즐기는지 가끔 지나가면서 문지르며 가더라|1 +죽음으로 보답받길|0 +걍 집에서 먹지 돈아깝다 왜저런걸 사먹노|0 +응 니도 처단됨 ㅅㄱ~|1 +한민관 존잘이네|0 + 이것처럼 진중권은 몇개월전 사실도 제대로 모르면서 그걸 맞다고 발악 발악 우기고 있을뿐임.|0 +일본--도둑|0 +진핑이 닮았네! 주머니 손 넣다 걸린 표정!나만 그럼?|0 +안 줘 돌아가|0 +나도 민속놀이하고싶다|0 +???:who 권고대로 코로나19로 불러라|0 +500급 카라반 같은데 일반 주차라인에 주차가 가능 한가요?|0 +이런 포맷?|0 +돈없어서 얼마벌지는 못하겟지만 그래도 10억이상은 벌었을듯|0 +팔라우는 남태평양쪽 작은 섬나라|0 +재는 관련 직종인 듯|0 +아닥? 니첨부터 영어만써놓고 ㅋㅋㅋㅋ 정신승리추하노|1 +원래 집이 작으면 커보이는 거란다~ㅋㅋ|0 +난?|0 + 여성 우위고 뭐고 결혼 자체를 안함|0 +다까끼마사오는 밤꽃 향기를 좋아했지..|0 + 그리고 나 홍어 아니다 내가 쓴글 정독 해봐라 이기야|1 +와 이런 중고딩이쓴거같은 개무식한 개똥글도 일베가고 일베 ㅈ망햇네 ㄹㅇ|1 +흠 그것자체가 매국노인건 아냐? 그라고 특이점도 최소 30년은 지나야 올것같더만.., 애초에 많은 사람 들이 살기 힘들어 한다면 그국가는 절대 선진국이 아니야|1 +30에 공장가겟네 ...ㅠ|0 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 난 24살 군인이데 개백수련 한심하네 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +걍 실축 강팀인 애들이 상위권이네게임이랑 실축은 전혀 다른데에이바르가 우승 할수도 있는거고 ㅋㅋ|0 +니말대로 친구하나 사귀고 가는게 더 유익하고 재미있을거같긴하다|0 +아주대 의대생이면인정 고3이먼 뒤진다|0 +좆같은 니거쉐끼|1 +또 시작이가 ㅋㅋㅋㅋㅋㅋㅋㅋ|0 +제대로 수사하려나 모르겠네|0 +ㅇㅈ 좆댔음 ㅠ|1 +여기서도 문재앙 이지랄들 하네 정치병 에반데... 문재인 싫은건 나도 싫은데 굳이 여기에서까지 ㅈㄹ들을|1 +계속오르니잘안팜 ㅈㄴ비싸게내놓던가 그런다 이번에 24평25억에도 사는거나옴|1 +느검은 내가 지켜주마|1 +일본av에도 난쟁이 나왔었잖아ㅋㅋ|1 +저번에 키스방 갔는데 얼굴은 ㅍㅅㅌㅊ 어린여자고딱 저거랑 비슷한 몸매에 피부 ㅆㅅㅌㅊ 나왔는데진짜 한시간이 황홀하더라|1 +미니가 훨씬 잘먹힘|0 +동물적직감이란게 있는거 같음|0 +숏패딩 ㅋㅋㅋㅋㅋㅋ존나 클론임슬랙스 +반스 조합 이상으로 클론임|1 +즐긴만큼 고통을 감내하거라 아마 평생 주홍글씨로 남을듯|0 +농사보다 사냥이 먼전데 기간은 개가 더 길지|0 +흐아 ㅅㅂ 당장 흙동네 집알아보러간다|1 +병신, |1 +오늘부로 용접공 이미지 떡상하냐?|0 +오바마 대통령은 왜 우리가 한국의 학구열을 배워야 되는지에 대해 여기에 이유가 있다라고 했다 대강 이런뜻 아닌가.?|0 +깜빡이도 켜져있구만|0 +야 일반 uhd티비 최상급이 qled최저등급이랑 동급이야그라인 위로 q70 q80 q90있는거고...;75인치 q80이 250인데;로컬디밍도 없는 저거 얼마에 삼?|0 +맞는말|0 +ㅇㅇ 느금마 기생충 구더기 곱등이 연가시 엑시구아 바퀴벌레 시궁창 쥐새끼들한테 애미 애비가 창자까지 돌림빵쳐당한 유사인류 씹김치년 에미뒤진 걸레갈보개씹창년 허벌보지 니미 씹창 좆물통을 사시미로 토막내고 도려낸 다음에 마체테로 찢어죽여버릴 육변기 개갈보지 씹창년새끼 ㅋㅋ 그리고 그 시체를 닭모이통에다 쑤셔쳐갈아버린 다음에 니 애비 십이지장에다 뼈채로 쑤셔박아버릴 육변기 개갈보지 씹창년새끼 ㅋㅋ|1 +그래도 집에서 놀지는 않으니까 별 상관없긴해|0 +멍석말이의민족 실토할 때까지 팬다. 그게 일상이었다.|0 +잘빠는여자 구별법이나 특징 머냐? 입술 두꺼우면 잘빰?|1 +https://www.google.co.kr/amp/www.donga.com/news/amp/all/20190625/96168051/1 선동이다 3심에서 유죄라고 다시 2심내려보냄|0 +ㅇㅇ 일단 희망을 갖고 싸우면 이긴다고 본다|0 +고급 인력|0 +지금 연락하는 여자랑 폰섹 엄청 많이했는데만나서 호텔 들어가서침대에 서로 마주보고 앉은다음에핸드폰 잡고 서로 쳐다보면서 폰섹 하자고 했더니"아 엄청 부끄러워 ㅎㅎㅎㅎ"하면서 좋아하는데또 야하게 할만한거 뭐 있을까?무선 바이브레이터 같은거 사서 끼고 밖에 나가서 해볼까?|1 +전라도도 틀딱들이 있을텐데 나이쳐먹고도 문재인 빠는거 보면 연식을 똥구멍으로 먹은게 사실상 맞음 |1 +달창단임|0 +폴라도 개새끼들에 비해서는 터키는 양반중의 양반임. 그새끼들은 입으로'라도' 형제국이라 하지.|1 +ㅋㅋㅋㅋ 그러다 cctv 돌려보고 악성 민원인으로 찍힐라|0 +리뷰관리 꼼꼼하게하는곳도 있더라|0 +얘 말하는거 너무 커여웡.. 입에 내꺼 물리고시펑|1 +제발 부탁인데 가만히 있길 ㅡㅡ|0 +중립이라 하는거 보니 ㅇㅈㅅ이네강사중에 자기가 중립이라고 말하는건 한명밖에 없을껄?ㅋㅋㅋㅋ다 지들이 보수라고 말함|0 + 돈은 뭐 내가 술 담배 안하니까 고대로 다 모았고|0 +캠리하브도200할인있다이기야 음.. 연비야 차크기가있으니 캠리가좋다쳐도 그랜저 159마력이노.. 소나타도 160마력대로 힘딸린다는데 그랜저는 159마력이노.. 캠리는178마력이고, 흠.., 글쎄..|0 +단교하자.|0 +그렇지만은 않다. 운동으로 충분히 재활 치료 가능 |0 +우리 몸안 mechanism 중 스트레스 아닌게 있음?|0 +안착해졌노|0 +하트랑 크로버 그려진거는 뭐냐|0 +매드아이 냥이 화장실모래통 냄새 엄청난대|0 +복도쪽에 앉으면..어깨나 허벅지에 승무원들 지나갈때 스친다..다만 안좋은점은 개매너 승객들이 치고 지나다녀서 간신히 잠들었다가깬다는거|0 +ㅋㅋㅋ 인강강사랑 용접공은 갭이 크지 ㅋㅋㅋ|0 +하긴 니들이 사람색히들이냐|1 +햄버거 많이 먹으면 종기남|0 +자기 남친 자랑하는거네글쓴여자 속마음은 난 내 남자친구가 청소하고 밥하고 설거지하고 다함^^존나 부럽지 이년들아 ㅋㅋㅋ이거임|1 +중앙대면 1등급인데 ㅋㅋㅋ|0 +애 재워놓고 갈정도는 아닌데|0 +닥쳐!! ㄴㄷㅎ~|1 +5ㅐ월정도 막아주면|0 +꼬추 헐겠다|1 +어디가세요ㅜㅜ|0 +실제로 김대중이라고 했는데 김재중이라고 한거라고 구라친거라는 생각은 안해봣노 ?|1 +집앞하수도에사는 바퀴벌레 신경안쓰듯 눈앞에만 안나타나믄 된데이|0 +어디서 교과서 들고나와서 받아적고있어ㅋㅋ|1 +설ㅈㅎ|0 +개터졌네ㅋ|0 +씨바알..노무노무 술프노... 오늘 저녁은 꽂등심이다ㅠㅜ|1 +이게 유명한사기냐 별 거지같은놈들도 다있노50만원짜리 벽돌이노|1 +준 라보 이름참.|0 + 넌 일제의 불합리성까지 빨게되는거지 이 빡대가리 혐한일뽕 새끼야ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +양복에 눈까리만있는거 개웃기네 ㅋㅋㅋ|0 +테팔샀냐?|0 +이번에 현대차 cvvd 엔진 열효율 40프로 잡았다더라대단한 결과물임|0 +근데 수사권이 넘어가면 어떤 이유때문에 국민만 힘들어지노 ??? 이야기좀 ~~|0 +1절만해라. 존나 처맞기전에|1 +프롬프터가 어딨는데 |0 +시험출신이고 나발이고 다 조져야함 ㅋ|1 +65kg = > 65g 의 2~3배면 130g~195g 이런식으로...|0 +한달쯤 전에 돈이 필요해서 천주 팔았는데 그때 54,600인가 그랬는데 5백 손해본거같아 억울하다이기|0 +헌법을 김제동한테 배우는데 저정도면 화려한 패널 맞다|0 +노예 ㅇㅂ|0 +난 취중 고백 이런거 존나 이해안감 술취해도 난 어느정도 뇌는 컨트롤 되던데몸은 안따라줘도|1 +ㅈ같은 아메리카요가 본점이든 지점이든 ㅈ되길 바란다|1 +꼭 보면 엠생/백수/모쏠아다/그지새키일수록 얼굴에 집착을하더라 ㅋㅋㅋ|1 +한양대안산좀 업그레이드해줘 제발부탁|0 +조안양씨성은 피해라|0 +니들 때문에 나라 작살난거 안보이냐?|0 +솔까 까는건 좌좀이 제맛이지깔게 노무 많거든|1 +그럼 넌 과거 박정희 정부도 부정하는거네 병신새끼야 ㅋㅋㅋ 그리고 빤스런? 씨발 7시 절라도식 정신승리 지렸노? 씨발년이 김대중 개새끼부터 해보자 ^^ 그리고 병신아 프랑스는 마크롱이 우파인데 뭔 좌파 복지야 병신새끼가 그리스가 포퓰리즘으로 좇망한 팩트는 절라도 염전에다 팔았노 이태리도 좌파 ㅈㄹ 하다 망해서 짱깨한테 털리는데 니 씨발 분탕이지? ㅋ|1 +슬쩍 얘기중에 권시장 얘기하면 몰라요~~ ㅋㅋㅋ|0 +근데 저말듣고 대학포기하고 용접배워 호주가면 현실적으로 인생 발전하는건 사실이지|0 +대한민국 권력서열 1위가 총장인줄 알았더니만. |0 +다중이 부캐 신고 완료. 엠창새끼 ㅂㄷㅂㄷ하다 완전 좆망했노 ㅋㅋ |1 +ㅇㅇ|0 + 힘들다고 휴직계를 내도 그 업무가 돌아간다는게 바로 그만큼 쓸모없는 공무원들이 많다는 것 아니냐?|0 +이 개씹새끼 감성팔이로 일베 두 번 왔네 딸 팔아서 일베 렙업하려는 호로새끼 ㅋㅋㅋ ㅁㅈㅎ 쳐먹어라|1 +거 참 상황 머중이스럽네...|1 +10대별로없는것같은데ㅋㅋ|0 +무인도되뿌라|0 +찐따는 ㅇㅂ |1 +어디가서 민폐 끼치고|0 +그딴거 필요없어 남이 알든말든 ㅅㅂ|1 + 내가 말했지? 계급 이라는건 같은 사람으로써 따지는 것이지|0 +맨앞 일베마크하고 있는 셔츠남 두분 근황이 궁금하다|0 +햐..봉하마을 뒷산 배경으로 아주 실루엣이 제대로 나왔네|0 +ㅅㅏ회복무요원.ㅠ|0 + 쟤가 용접공볼때마다 7등급이하 나보다 덜떨어진새끼라고 생각할거아님|1 +그돈으로는 현재 기업들 채무 정리하기에 택도 없다고 깔거고|0 +이색희 이거 가입하게 생겼.. 이 아니라 운영하게 생겼네.|0 +양아치들도 생기부 잘써주거나|1 + 그렇게 작은 병 큰 병 운운하면서 마치 무슨 중증외상이 열악한건 순리이니 따라야한다는 식으로 바람잡지마 돌팔이 새끼야.|1 +지가 무슨 박사학위 있는 마냥 말하네?|0 +저건 혁명이야|0 +더불어 다카끼마사오 해외계좌 찾으면 대박일텐데~|0 +하나를 가르챠주면 하나라도 알면모를까 알긴커녕 지랄부터 하고보는게 한국인임을 잘 보여주는 예구나|1 +ㅁㅈㅎ 누른 전라도 씹버러지만도 못한 놈들은 저걸 실제로 봤거나 알고 있고 또는 직접 실행에 옮길 수 있는 잠재적 개쓰레기새끼들임.|1 +이마저도 최근 미국 민영 셰일개스 업체들에 의해서 무너지고 있다.|0 +하.... 꿈의 계약직이네|0 +전세계 어디도 저렇게 돈 많이 들어가서 적자보는 의료 시스템은 없음!고로 이국종 또라이|1 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ막짤 좆되노 ㅎㅎㅎㅎㅎㅍ|1 +븅신아 꼭 풀어말해야 되냐 개인과 개인의 성격에 관한 문제란 말이지 거기서 인간을 나누고 지랄은 뭔 개소리빠노 껒여라|1 +화장실에 간다..|0 +똥남아 갈때 가끔씩만 비지니스 탔는디. 얼마전 벳남갈때 국적기 이코탔는디 티켓팅 할때 앞에 베트콩 할매 할배딱봐도 한국 시골시집간 딸내미 집와서 존니 농사짓고 본국 가는사람처럼 보였는디 암내가 시박 쩔더라 탈때 설마했는디 내 두줄앞인대도 살짝 냄새옴만석인디 만약 내옆자리였음 진짜 뱅기 뛰어 내렸음이후로 똥남아갈땐 무조건 비지니스 탈꺼다|1 +네다음 시골.|0 + 현재- 지금 부동산 살 시기 절대 절대 아니야 갖가지 규제가 발목잡는다! |0 +네겐 다른 선생이 필요없으니 성령님께서 네 선생님이다.|0 +순경이하는게아니얌 경찰대출신엘리트들이하는거지|0 +목매달고자살해라|1 +깨끗한 나라 화장지도 손절 했습니다 ...|0 +그래도 개돼지들은 가만히 있자나 지들이 지옥으로 입문하는지도 모르고 월몇푼 주니까 좋다고 아가리 처닫고ㅋㅋ|1 +야, 꿀벌|0 + 컷트는 이발사가 훨씬 우월함|0 + 나는 분명히 말했다. 사회 어디나 모순이 있고 암막에 가려진 데가 있다고. 의료나 징병 모두 국민재산생명에 결정적으러 엮인 거라 당연히 희생을 한거다. 그걸 의무라는 표현으로 듣기 좋게 묶어둔거다. 니 말대로 인정하자고? 뭘 인정하는데? 결국 다 돈이라고?|0 +지들이 좋다는데 왜 난리임 ㅋ|0 +서열1위네 좆냥이 찍소리도못함ㅋ|1 +정부에 반항할수없는 한국기업 삼성은법원에서 발부한 강제명령서만 있으면보안 다 풀어주겠지?괜히 좌익 네임드들이 아이폰 쓰는게 아니군.이재명도 스마트폰은 죽어도 지켜라 라고 말하지않았나.폰 하나 뺏기면 이제 영혼까지 탈탈 털리는 시대다.폰 안에 들어있는 혐의와 무관한 모든 개인정보들 다 뒤져본다.|0 +ㅋㅋㅋㅋㅋㅋㅋㅋ. 여갤에 내 시민권 딱 박혀있으니까 팩트로는 뺴도박도 못하고 ㅂㄷㅂㄷㅂㄷㅂㄷㅂㄷㅂㄷㅂㄷㅂㄷㅂㄷㅂㄷ 데면서 인신공격밖에 못하는 대꺠문보솤ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ. 주모!!~~~~~~~~~~~|1 +흠 난 그래도 소비자 입장이 이해됨|0 +그러게 과학적입증없이 그냥 완치했다면 믿음이안감 저약때문에 완치한건지 과학적으로 증명이 불가하다|0 +문동무! 이러니 공산주의 안좋아할수 있음메까?|1 +근데 머리가 나빠서 공부 못하면 기능은 더 못할 확률 높음|0 +어휴... 저 쓰레기젤을 빠는넘이있네고추 썪는다 |1 +혜지픽 ㅇㅈ?|0 +이수진 프레임 잘 잡았네|0 +걍 외장하드 사서 보관해 이것들아.. 무슨 클라우드여?? ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +사후 약방문 하시지 말고|0 +집있다고 안되면 집팔고 그걸로 놀고먹다가|0 +이참에 주한미군도 싹다 일본으로 넘어가주면 좋잖아~|0 +근데 결국 큰틀에서 못벗어난다|0 +편하게 말하는거지 저렇게 존칭 쓰는게 안 이상하냐? |0 +ㅇㅇ 손때야되는일에는 손때야되는데 그러지못한케이스|0 +일단 불편하신그분들이 좀 달려들고있으니 오래갈듯|0 +상추도 고기싸먹을 때 식물적 직감이 있나?|0 +설마 얘가 망할준 몰랏는데근데 존나 잘먹더니 언제부터 안먹고 뱉던디장에 문제온거아님??|1 +@봉봉주세효 ㅋㅋㅋㅋㅋㅋㅋ|0 +그냥 너가 못배워 쳐먹어서 해석을 병신같이 한건데|1 +애미디졋네 ㄹㅇ|1 +전형적인 찐 ㅋㅋㅋㅋㅋ|0 +담배피면서 뭔 정크푸드안먹는다고|0 +대부분 그러다가 나중에 감 그래서 존나 좃같은거|1 +7등급 대가리면 용접기술직도 못함|1 +특히 저기는 정의당때문에 5프로 지고들어가는 개같은곳임 정의당이 문제 ㅡㅡ|1 +존나 미친새끼가 맞는거 같은데ㅋㅋㅋㅋㅋ|1 +그건 너가 작으니까 그런거고|0 +씨발 라이브릭은 광고같은거 없냐|1 +육부가 째졌겠다|0 +둘다 일베충들이 못하는건데?|1 +정신좀 차려라 징징아|1 +@누드트라다무스 ㅋ|0 +ㅅㅂ 억울하네 난 2년전에 산건데그레이 숏패딩|1 +금수보다 못한 쓰레기들 이겠지요|1 +진짜 이세상에서 중딩이 제일 맛있음. 갓생리 개통할 시기지 중1이 제일 맛남|1 +일베리스트 만들어서 한 10년정도 취업 안돼봐야 정신차림.|1 +https://youtu.be/Y8TbkR7qJt0|0 +힘내시길~|0 +아이큐 낮은 쿵쾅이들 그림인거 같은데 ㅎㅎ 그래도 뭐 다 공개 하는게 맞지|1 +네 다음 사람 죽이는 동물 BEST 4 나오세요~|0 +몇군데 알려주라. 적어놓게.|0 +신종코로나 사태로 나라에 돈이 없어 자진 납세를 하려는 아름다운 장면이네요^^아직 살만한 세상이에요|0 +개새들이여|1 +30인 규모 씹좆소가 더 맞을지도 ㅋㅋㅋ 일은 존나 빡세도 오히려 씹좆소들이 쓸데 없는 저런거는 없음 왜냐면 일이 존나 빡세고 작업 환경 열악해서|1 +너무 많이 돌고 돌아서 캡쳐 화질 넘 안좋아졌네..|0 +저런 늙은이들이 이나랄 망친다~|1 +아줌마? ㅋㅋㅋㅋㅋㅋㅋㅋㅋ 시간 많이 지났는데? ㅋㅋㅋㅋㅋㅋㅋㅋ 그래서 인증은 언제? ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +현빠절감 조카튼거 싸질라따고 들 어써요|1 +중부고속도로 하행 진천터널 즈음 터널 진입하면 허구헌날 막힘 시발 뒤에 화물차 따라오고있으면 존나 무서움ㅋㅋㅋ|1 +어허|0 +답글 글씨체를 봐라 저게 애새끼가 쓴거냐?"빨갱이새끼가 쓴거지 ㅁㅈㅎㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉㅉ|1 +근데 밈이 먼뜻임|0 +페북에 떠도는 유머글 ㅁㅈㅎ ^ㅡ^ㅗ|1 + 너가 연락안하면 연락오는사람 없지 ㅋㅋㅋㅋㅋ|0 +나타났다 잡히고 잡혔다가 사라지네|0 +공산화된 헬조선이라 처벌받을일 없음 그냥 정준영 혼자 독박씀|1 +반대한 놈은 극복하지 말라는 소리죠?|0 +너가 열심히 산 걸 그냥 타고난 거라고 폄하하는 거처럼 보일 수도 있고.|0 +혜경궁은 그냥 말만함. 실천을 안함 언론에 홍보비 주고 언플은 ㅈㄴ함|1 +사무라이 간지초딩들이 환장했지 ㄹㅇ|1 +지금은 작년에 중진공에서 20억 투자받은 제조업사장임. |0 + 시키드나?|0 +비타민 무용론도 있던데 심지어 요즘은 음식못먹는사람없어서 필요없다는의견도있고 의사들중에 이런경우가있더라|0 +쟤가 악의가 있었으면 , 용접공을 다 죽였겠지 , 용접공이 나쁜 직업이라고 놀렸겠냐 ? 공부 안 하면 힘든 일 한다는 뜻이잖아|0 +신내림이라도 받았노?|0 +고기김치만두 이새끼 좆병신 30대 중반 쓰레기놈이다.|1 +담날 맨정신으로 다시 한번 고백해라 술쳐먹고 하지 말고|0 +공직자가 공직자가 아니고 전문가조차 전문성이 떨어지는 곳에 인력들을 배치하니까 이 사단이 나는거다. 아마추어 정부 수준도 아니고 동네 아는 바보 병신들 모아다 꾸린 집단임|1 +흠냐..|0 +나무위키 내용중에|0 +와 씨발 중국이냐. 진짜 죠센징들은 날이 갈수록 퇴화하노. |1 +진짜 ㅋㅋㅋㅋ 댓글 꼬라지 보면 정말 '한'많은 한민족이 따로 없음 ㅋㅋㅋㅋㅋㅋ|1 +곱하기 50해보고 들어갈지 말지 잘결정들하세요.|0 +농어촌특별전형이면 내신이 높고 수능이 낮은 거지내신 6~7이라 해샀노대구리 안돌아감?|0 +ㅇㅇ 사전에 합의된거고 가세연 시청자들한테도 한마디 했다|0 +좆셴 미개한 개돼지들이 원하는거임|1 +97년생인데 안다 ㅈ꿀잼 이였는데|0 +전기차가 나중에 상용화 될때까지 기다려라 지금사면 병신테스터 일뿐이다.니네집 주차장에서 개인충전 가능하고 있고, 회사가면 주차장에 개인충전 가능하면 모를까...장거리뛸땐 미리미리 목적지주변에 검색하고 알아봐야하고, 자리를 가더라도 다른차충전중이거나 충전완료되었는데 그냥 안비키고있거나일반차량주차해되어있거나, 그럼 일일이 전화해서 빼달라해야하고, 말도안통하는 곳에 해외여행 갔는데 배터리가 50%남아있어도 보조배터리가 없으면 항상 불안한 느낌이라면 이해하려나..차라는게 편의성인데, 이런 불편함을감수해야하나...돈? 기름값아낀다? 음 일부분 맞기도 하고 틀리기도하다. 전기세가 싸니 유류비를아끼는건 맞는데, 차가 일단 비싸다. 수리비 좆된다.아직은 내연기관타는게 맞고, 배터리기술 올라가고 값떨어지고 주차장에 무선충전가능하고, 상용화된시기오면 그때사는게..답이다.|1 +개독게이 또왔노 이기|1 +ㅋㅋㅋㅋ 돈내 놓으란거지 7등급 도 못 나오면서 떡밥 물었지|0 +미씨 원탑을 까노|1 +청진기 하나사서 폭딸해라게이야|1 +아개재밋어 ㅋㅋㅋㅋ|0 +국내 탑5 ㅋㅋㅋㅋㅋㅋ 포방터 게이 돈까스 안만들고 여기서 뭐하노|1 +미친새끼 왜사냐 나랑현피뜨자 씨발련아 내가 도저히못참겟다 인간이길포기한새끼야|1 +노벨과 개미인가 시발 엄마가 강제로 뭐 했길래오면 풀고서 내놓으면 채점해준대서 별 죶같지도 않은거풀지도 않고 풀어도 내놓지도 않았는데하도 안 내놓는다고 ㅈㄹ하길래 북한 잠수함과 간첩이 우리 나라에 침범하였습니다 어떻게 해야할까요 주관식 질문에보이면 잡아서 칼로 찔러 죽여야한다고 적고 내놓으니까 다음부터 채점해준단 소리 안함 시발 아주매미년|1 +7등급이 뭔 대학 타령 ㅡㅡ|0 + https://en.wikipedia.org/wiki/Iran 지금 이란은 5500달러. 근소하게나마 ㅅㅌㅊ|0 +지들이 동물이니까.|1 +김씨~ 아무도 자네 연봉 안물어봤어~ 닥치고 지게차 기름좀 가져와바|1 +단순한 논리인데 감성을 섞고 있노|0 +저분 학부어디나옴?|0 +발바닥 어떻게 ㅁㅈㅎ 당함?|0 +응ㅋ나있다|0 +일베까지 테슬라가 언급되네.. 매도 신호인가...|0 +부작용인듯ㄷ|0 +글은 이렇게 쓰는 것이다 알려주는 좋은 예인듯 합니다.|0 +어차피 아이폰 미만잡이다. 갤럭시는 지구 망할때까지 아이폰 못이길듯. 내가 갤3때부터 이제는 갤럭시가 아이폰 능가했다는 소리 하도 마니 들어서 아이폰4 쓰다가 갤3로 갈아탔는데 그게 진짜 젤 후회하는 짓중에 하나다. 아이폰은 오래써도 그래도 쓸만하단 소리 나오는데 갤3는 진짜 와 1년 지나고부터 진짜 매일 벽에 던지고싶은 충동 참음....조루배터리랑 버벅댐, 렉 등등|0 +예쁜이 거르고 진짜 ㅆㅅㅌㅊ다 사랑해|0 +뭐 선천적으로 여성스럽거나 여자역할을 하고 싶어하는 동성애자들 그럴 수 있다고는 생각 드는데, 자기들이 옳다고 생각하는 것등 PC주의로 남한태 사상 강요하는것 개극혐. PC주의자 새끼들은 존나 말한마디 단어 하나하나 사소한거에 다 불편해 하는데, 본인들이 남에게 자기 생각 강요해서 다수 불편하게 만드는건 생각도 못함. 자신들의 사회에서 소수이기에 무조건 "선"과 "정의"의 편에 있다고 가정하기에, 남에 대해선 존나리 엄격하게 도덕적 평가 내리면서, 자신들의 행동에는 존나 관대하지. |1 +사진이 잘나온건가 옆에 음메페가 있어서 그런가? 두번째 짤은 베지터 같은데?|0 +자료 이해 하나도 못하면서 퍼오는건 잘하네-ㅁ-;;|1 +기독교 목사가 떡치고 다니고|1 +7등급 이하는 ㄹㅇ 자기손을 용접하지 절대 일 못함.|1 +고종이면 모를까 지랄을 한다|1 +핸드폰??|0 +빙신새키들 남자직업 비하 하는지도 모르고ㅋㅋ 좋다고 난리네폐미들이 군인까는거랑 똑같은거다 옹호하는 빡대가리새키 나가뒤져라|1 +몇년이나 몇달 대충 예상 안됨? 아님 들리는 썰 같은거라도 |0 +호주에 한국 창녀 많아??|1 +아 합성 혹은 총안맞은걸로 짜집기한거임??|0 +아직도 멀었네요. 전에 이슈화가 됐었는데 다시 묻힌건가요? 아니면 유사사건이 또 일어난건가요?|0 +그냥 조용히 알아서 잘살길바라자 이게맞다 저게맞다 할필요가있나..이미 이친구는 연예계에서는 끝났는데 욕은 욕대로 다먹고 조롱은 조롱대로 다당하고 그냥 조롱이라도 하지말아달라고 말한번햇을수도있지 뭐 믿나안믿나 다따지고그래|0 +ㅋㅋ 패기넘치게 공지 올렸으니 패기 넘치게 망해봐야지|0 +시발 이국종도 빤스런 각 잡는 나라인데 ㄹㅇ 경찰 해서 완장질 좀 해야겠네 ㅅㄱ|1 +때리다 지치면 힘나는거먹고 또 때리고싶노 |1 +공항 식당에서 식사하고 있는데우리 테이블 좌측넘어에서 우측넘어에 있는 일행과 대화하더라|0 +제발 콘돔 좀 사용합시다. |0 +문재인디졌당 추천|1 +퇴사하고싶을때보는 영상 그냥열심히일하자https://youtu.be/PMrMhs9BeqY|0 +전씨는?|0 +바이러스 짱깨 , 지들끼리 치료제 처맞고, 사재기 부추기고 아 짜증난다.|1 +뭘 달아놨길레 아프노?|0 +난자 얼렸나|0 +가짜뉴스 안보는 재미요|0 +베충이왈:이것은 조작이므니다!! 빨갱이 조사기관 이므니다~ 천황폐하 만세!!|1 +에휴 종교 관한 글은 안보는게 답 그냥 볼때마다 기분만 잡침 ..|0 +크으으으으으~!!|0 +ㅋㅋㅋㅋㅋ 너 용접공이냐? 존나 웃기네. 잘가 병신아.|1 +와우|0 +똥양인으로태어난거부터가 인생조진거임ㅋㅋ|1 + 좌측이야|0 +아니아니이~~^^ 기호는 사람이쉽게 알아볼수잇어야 하는데 누가 다이아몬드표시가 전방에 행단보도잇다고 해석하냐는 말이야^^ 마름모꼴 하고 행단보도하고 연관성없자나아^^ 그래안그래^^|0 +이새끼들 올림픽 드라마도 찍었던데 좆되버리겠네 ㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +배달비 보다는 사실 음식 쳐 빼먹는 못 쳐 배워먹은 씹새끼들 땜에 욕함|1 +나 시골사는데 시골이 공기더좋은거 맞는데 ㅋ|0 +공부에서 낙오된게 사실 아닌가?ㅋㅋ|0 +보......보....가..|0 + 강의력도 없고 얼굴만 믿고 깝치는 거라 더 좆같음|1 +고소당하신...... 위추드립니다|0 +ㅈㅈㅂㅁㅈㅎ 어제꺼를 쳐올리네|1 +통계학과|0 +저런년들 팅기다가 일단 빠구리 시작하면 신들린듯이한다 |1 +내가 지금 노트북 켜서 어코드랑 가격 비슷한 노블레스 봤는데 K5는 어라운드 있는데 어코드는 없다|0 +제품을 구매하는데 사전에 정보를 수집해야 한다. 캬|0 +집세 및 생활비로 나가는돈 떼면 남는게 없음|0 +맞는 말이긴 한데ㅋㅋ 그 7등급을 5등급 또는 1등급 까지 올리는게 지 역할 아닌가?|0 +니뽕 1등해!!!|0 +바가지 쓸일이 없는게 택시는 그랩, 숙소는 숙소앱, 기타 다 정찰제로 되어있어서 흥정할 일이 없음.|0 +냄새가...|0 +애비로드 세번갔는데 사진찍으러가면 차들 다 기다려주는데 딱한번 성질내는 할머니봤음. 빨간색 4컨버탄할매|0 +저정도 급여를 받으며 일할 새끼들은 |1 +좆같은 김치년들한테 선택받는게 메리트 있냐. 선택해도 거부다|1 +응 망하긴커녕 환율조작국지정 해제되서미국이랑 중국 화해분위기~~~무역전쟁도 올스탑이다이제ㅋㅋㅋ중국경제성장 오지게될듯|0 +점입가경|0 +대한민국을 다민족 혼혈화시키는것이 현실적이노?대한민국이 다민족 혼혈화되면 누가 가장 좋아할까?|0 +짤은 꼴리노|1 +황교활 ㅋㅋㅋㅋㅋㅋ|0 +또라이 식히|1 +남한테 집안 사정이야기 하고 가족까는 것은 일베충들도 똑같지 않냐?|1 +맞는말인데 왜|0 +확실히 눈떨려서 미그네슘 사 먹는데 안낫는거야그래서 그냥 바나나사머고 했더니 사라지|0 + 중견기업의 경우도 사장이 야심이 있는 곳이면 대기업 못지않게 바쁜데 걍 대기업 1차 벤더로 안주하는 곳은 업무강도가 대기업보다 낮고.|0 +재밋겟는데 ㅋㅋㅋ|0 +그래서 나 피씨방에서 로그인할때아이디가 ilbe비번이 nomuhyun 이면아이디의 i치고 비번의 no치고 다시 아이디의 lb 치고 비번의 hyu 치고 아이디의 e치고 비번의 n치고 이런식으로 왔다갔다해서 들어감물론 클릭 뿐만 아니라 탭키와 쉬프트 백키도 활용해서 페이크 몇번주고그럼 해커들도 정신이 오락가락 하면서 나한테 쌍욕박음|1 +본격 연애 방해 프로그램..|0 +홍위병들 당할수가 없어|0 +첫짤 저렇게 생긴 일게이들이랑 여태 히히덕 거리고 있었다니ㅜ자괴감 드노...|1 +이명박 외교 부분은 박근혜보다 더 잘했다|0 +민노총공돌이 새끼들아 안사새끼야|1 +변절탈북자드립보니 쟤도 진성이더만|1 +코로나 배양국|0 +나는 장모년이 지랄하길래 장모님은 왜 그거에 예민하신데요? 이러니 아무말 못하더라|1 +함의|0 +지지하는 대구 인간들 수준도 참!|1 +댄스그룹 아니었냐??|0 +ㅈㄱㅈㅈㅈㅂ ㅁㅈㅎ|0 +그렇게 따지면 사범대도 3~4등급이면 가는데? 선생되면 걔네가 1~2등급까지 가르칠텐데 그것도 니 논리면 모순아니냐?|0 +그거 가족전체 6000원으로 관리비포함 운영되는거다 게이야ㅋㅋ|1 +마치 윾투브를 보는거같네 ㅋㅋ 결국 다 짤림 ㅋㅋ|0 +애플이 후원햇다는 증거 있어?|0 +시골경찰 순찰시 음주단속 잠깐씩 |0 +머래 병신새끼|1 +이런 개소리하러 오겠죠~|1 +난 가끔 고백 받는데. 크게 몇가지 유형으로 나뉨.멍청한뇬 1 : 여러명이 손잡고 와서 오빠 누구랑 사귈래요?레알 미친뇬1 : 네가 뭐뭐 어쩌지만 난 너랑 사귀어 줄 수 있어.(지가 손해지만 사귀어 줄 수 있다 지룰 레알 미친뇬임)못노는 뇬1 : 어렷이 있을 때 내 엉덩이 만지거나 나 핸폰 볼 떄 내 손등에 가슴 갖다댐. 불쾌함잘노는 뇬1: 술자리에서 스킨쉽 능숙하게 들어옴. 주변인 전혀 눈차못챔. 화장실 가서 확인해 보면 쿠퍼 샘. 술자리 끝나면 어찌된 일인지 단 둘이 걷고 있음. 와 이게 리드 당하는거구나 헤헤개인적으로 잘노는뇬이 최고임|1 +시진핑 같은데?살아있는 놈도 심령으로 등장하노...|0 +베트콩만 참가하길 빈다!|0 +아래 댓글 참조|0 +그럼 니가 키워야지|0 +저놈없으면 최전방 자원이 마샬 한놈밖에 안남는데 일단 붙잡아 보려고 하긴 했겠지|0 +미개한 유럽짱개들...|1 +앗!아까그새기잔어 개새기야|1 +글쓴게이는 저 상표가 무지맘에들었나보다 근데 비싼걸 어쩌냐 ㅠ|0 +바보같아서 모르는경우도 있음|0 +성괴티 오지게 나네 씨발ㅋㅋ|1 +ㄴㄷㅎ|0 +추격자 도망자|0 +부들대는 새끼들 518.6974%지잡대미만 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +신라젠 압수한게 작년 여름임 작년 가을도 아니고 여름 그동안도 수사안했음 드디어 해체해서 마음편해졌을듯 윤이 ㅋ|0 +레깅스년들은 맨날 허리꺾고 엉덩이 빼고 지랄임헬스장에서 스쿼트할때도 허리꺾고 지랄들하대요즘 누가 허리 꺾으라고 가르치냐물론 이쁜애들은 뭘하든 상관없슴|1 +황교안이모지리라도재앙이같이 사악한 인간은아니다아주 끔찍한 인간 문재앙|1 + 니가 가져온 베드로 후서 3장 16절은 마지막 때를 알지말라고 하시는 말씀이 아니고 |0 +어린 나무 , 좆마니 나무 이식 작업은 전세계 어디서나 똑같이 한단다 무식한 새끼야 ㅋㅋ무식한 새끼가 경험까지도 없어서 과수농원 구경이라도 한번 해봤어야 알지 ㅋ|1 +상식이 통하는 사회에 나라다운 나라를 만들고 있어서 슬픔 속에서 희망을 봅니다.|0 +ㅂㄷㅂㄷ 거리고 가네ㅋㅋㅋㅋ|1 +난 직장인 인대 잔업꽉꽉채워서 500까진벌어봤다|0 +완전 가능!!! 4년까지 가능!!|0 +그게 왜 김도읍한테 지랄할 일이지?|1 +과학|0 +불가|0 +이걸로는 떡밥이 약해~~~~~|0 +좆치도 그런데 좆치는 양이 적자노ㅋㅋㅋ|1 +그쯤 되면 중이 절을 떠나면 되는데|0 +오케이~~ 거기까지|0 +그럴때마다 행복하고 고마운일 생각하며 버텨야지ㅋㅋ|0 +청소부 : (에휴 쓰레기가 늘었군)|0 +이논리 저논리로 봐도 홍씨 말이 다 맞았네 ㅅㅂ |1 +힘내보아요.... ㅠㅠ|0 + 존나 무식하고 인간 말종들|1 +조작|0 +3시리즈는 g70이랑 비교해야지 어디서 아반때를 쳐올리노|1 +미안,,그냥 죽어|0 +ㅇㅇ 맞음 이거임 시발 나도 중고나라 사기당했는데 역으로 조선족 씨발년들이 이지랄로 내번호 곱창내더라 ㅋㅋㅋ|1 +ㄹㅇ?|0 +이게 뭘까|0 +미국년인지 일본년인지 알수없다|1 +이병신은 그랜저는 풀옵 3.3 비교하고 3시리즈는 2천깡통을들이미네 이씨빨썅년이|1 +일본은 추락만 남았다|0 +쇼타꼬추조아?ㄷ|1 +ㅋㅋ 장난이얌 ㅋ ㅇㅂ|0 +싸이클롭스베 ㅋㅋ|0 +이미 한건 했잖아 ㅋㅋㅋㅋ 제 몫 했으니 이제 퇴장하면 재이니도 좋아할듯 ㅋㅋㅋ|0 +개소리 ㅁㅈㅎ|1 + 나와있네 병신아 그러니까 출처알수없는앱 설정이 체크 돼있으면 무용지물이라고 |1 +국민성이죠|0 + 나 한 7등급 되노?ㅋㅋㅋ|0 +저동네에서는 도지사까지 순탄하겠다. 우리동네였음 진작 사퇴촉구난리났겠는데|0 +ㅅ.ㅅ..사...삼성 ㅎㄷㄷㄷ|0 +현대마크에는 봊물안트짐|0 +이거 나쁘지 않은 생각인데?|0 +뒈져!!|1 +필력 괜찮겠습니까?|0 +세계에서 미국의 역할에 대한 지구촌 사람들의 인식이 좋지 않다. 유럽보다 훨씬 못하다. 이른바 강대국이라는 유엔안전보장이사회 상임이사국 5개국 중에서는 프랑스에 대한 기대가 가장 컸다. 미국은 영국이나 중국보다도 못했다. 러시아는 꼴찌였다.[출처: 중앙일보] "미국보다 유럽에 호감"https://news.joins.com/article/14343|0 +기술은 무슨 ㅋㅋ 기능이라고 해라|0 +근데 처녀도 아님|1 +물론 내가 하는 말이 단순 핑계라고 보일 수가 있어.|0 +그들만의리그|0 +드래곤볼gt랑 슬램덩크때문에 알게된 내가 좋아하는 유일한 일본인.. 안타깝더라|0 +그분들은 억울하게 깜빵간거지 ㅋㅋ이재명이 억울하게 범죄자된거니?할튼 방구석황제새끼가 뇌는 왜달고다니냐?이재명이 억울하게 2심에서 선거무효형벌금받았니?그걸 탄원서 제출했으니 욕처먹는거야병신새키야|1 +3대 500이면 일반인 사이에선 괴력 아니냐 ㅋㅋ|0 +사퇴하세요 ㅋㅋㅋ|0 +다들 좀 미쳐있는 게 사실. |1 +이건 마사지 언니야들이 몸타기 할때 쓰는거|0 +이새끼 나이도 존나 마니 처먹은 들딱이더만|1 +이참에 탈탈털자 제발|0 +저런 영업맨들은 환상으로 치장하는거지..|0 +저 썅년 상판떼기는 좀 지워주세요|1 +이준기 외모 서양가면 무시받는 얼굴이냐?|0 +애비.엑미가 능력이ㅜ되니.밀어주지..|1 +이거 ㅁㅌㅊ??마티치~|0 +내 자신의 행위를 신뢰한다면 그 사람은 천국에 가지 못하고 지옥에 가게 될 것임.|0 +리얼 쉐도우복싱 ㅇㅂ|0 +일본이 지금 평균 신규 채용보다 퇴직자가 더많아 현시점 고령화 일본이 너무 압도적이라 당장 취직자체는 확실히 많이 열려있음다만 단점은 전세문화는 우리나라뿐이라 자리 잡을떄까지 월세압박 좆될거임|1 +아마 엘리사는 아니셨을 듯...|0 +은근 맛있음 |0 +그러니까 그아저씨는 일나가러간거고 넌 일못얻어서 남았다는거지?|0 +ㅋㅋㅋㅋ 존나 병신이네'돼'가 되야함 에서도 돼야함이 맞다 병신아..맞춤법 못맞추고 있으면서 지적질하는 꼬라지보소|1 +선진국에선 저런 마인드가 당연한건데 ..왜 유독 한국에서는 여자는 힘든일 하면 안된다는 인식이 있을까...|0 +바퀴벌레들은 스물스물 기어나올 때 조져야 합니다.|1 +니미 씹이나 차단해라.|1 + 그상황에 총선 안지는게 이상한거 뮨재인독재를 막으려면 황이 잘해야함 근데 못함 그런 선거라도 이기는방향으로 가야함 근데 이기는 방향 뻔히 있는데 안함 할생각도 못함 티케이 100퍼 물갈이 하면 총선 과반은 그냥넘고 민주당 두자리 될수도잇음 근데 아니라고 우김 그러면서 하는건 불출마 이미오래전에 한 김무성만 욕함 진글 퍼오기 도배만 함 병신짓 ㅇㅇ|1 +수소차는 일본 미라이 미만잡 아니였냐?|0 +유투브 넷플릭스 안되면 안샀지|0 +장모님의 나라인가.../|0 +그정도면 최소한 인간같이는 사는 집이지|0 +문명인줄..ㅋㅋ|0 +조공아니다 용접봉으로 ㅇㅂ마크 그리는데 뭐 묻어서 코팅장갑 낀거다|0 +기득권은 더 좋아지기에 |0 +대답이뭐든 그딴거왜봄? 이 내 대답이다|1 +ㅂㅅㅅㄲ 용접이 뭐가 어때서 ㅉㅉ 정신과다녀라|1 +방송은 다 연출이고 대본이지~하면서 방송에 나오는건 그대로 다 믿고 죽일라함|0 +@포레스트31 븀|0 + 아 다르고 어 다른데|0 +아마 접대스파링이라 심리적으로 꿀리고 들어간것도 조금 있을듯|0 +틀짝 특징인간대 인간, 개인대 개인적으론 은혜라고 할 수 있지만그걸 국가적으로 확전시킴.그렇게 은혜로우면 미국 참전 용사 한 명이라도 초청해서 국밥이라두 한 그릇 대접하등가 씹탱들아~~~|1 +아 약값많이든다고 저기 써있잖아 병신아|1 +영원한 빛 만화가 베충이 맞는 거 같재|0 +공산주의 시작하면 사회 전반적으로 저꼴남ㅋㅋㅋㅋㅋㅋㅋ제대로 일하는 새끼가 병신이 되는 시스템임|1 +@봉봉주세효 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +여리면 살인못해 시발아|1 +구민들이 합심으로 순국선열 엿 먹이는 동네요?|1 +중국무술은 격투기가 아미고 합맞춰서 하는 공연임|0 +신천지 봉쇄 안하기, |0 +뭘 베네수엘라까지 가고 그러노|0 +유상철은 왜 안죽지죽을때 됐는데 너무 조용함|1 +그 옆에서 맞장구 처준 놈이 육군 장성 출신이었지요..|0 +초면에 보면 진심으로 베푸는 것 처럼 보임|0 +복도 자리가 언냐들 응디도 접촉하고 최고여|0 +나는 저 녹취보는순간 이국종의 의사가 아니라 정치인이란 느낌이 빡듬 의사들사이에서도 이국종 평판안돟음|0 +이참에 수리도 좀 하고 공사도 좀 하고 이불도 좀 바꿔라.........|0 +혼자의 희생으로는 한계가 있어. 이국종 교수 은퇴하면 누가 그자리 대신할려고 하겠냐. 일반회사에서는 그걸 사축이나 노예라고 부른다던데..|0 +외계인 고문한다는 샘숨 스맛폰 부서에있노?힘내라 게이야 세상에 쉬운게 어딨노ㅜ지랄맞은 부장게이 실력 좋아 그자리 올랐겠지만,본문게이랑만 마찰있는게 아니구,다른 사람들이랑도 그렇다면기냥 버텨라이기야...그래봐야 다 사람사는세상|1 +에효.. 니 직장이나 스와프해라 빙시나. 쪽팔려 어디 앞으로 글이나 써지겠냐. 종양이면 좃선이나 동악일보랑 스와프하면 되겠네. ㅉㅉ|1 +이미 눈빛이 만취했네...|0 + 1. 시?에서 운영하는 탁구 수업에 간 엄마, 이미 왕따 당한 50대초반 판교출신의 교통공사부장의 와이프, 그리고 타지출신인 우리엄마도 왕따당하고 그 판교 아지매랑 베프되다.|0 +애초에 인문계 고등학교에서 수능 7등급 나오지도 않지|0 +지금 박사모 가 문제니 |0 +이거 원글이 의게이가 ㅇㅂ에다 썼다가 각종 커뮤 돌고 돌지 않았냐|1 +처녀 구별법이나 특징 부탁해|1 +물뿌리는장면 백마표정보니까 흐뭇한표정이네|1 +저거 에비없다고 국장되는거 아님.어느정도 성적도 맞춰야한다대가리딸리는 일게이들은 어차피 못받으니부러워할 것 없음|1 +근데 배달비를 소비자가 다 내는 경우는 거의 없고 업주도 일부 낼텐데 업주 입장에서 배달직원 쓸때 나가는 돈이랑대행업체 쓸때 나가는 돈이랑 비교해야 되는거 아니냐?젤 큰 문제는 제품가격도 매장보다 비싸게 처받으면서 배달비까지 받아처먹는 새끼들이지|1 +공수처나 검경수사권조정이 진짜 개좆같은 거구나라고 이해하는 놈들많음.|1 +그리고 너는 아다일 확률이 높다|1 +독일하고 이탈랴 잘 생각해라!!|0 +아이고...|0 +홈리스들 관리 하는 곳에 보내면 되는 문제 아니냐 ?|0 +100만표는 이번에 날아 갔을듯|0 +공군 남색 아님?|0 + 안방에 집문서 꾸러미에서 찾음 ㅋㅋㅋㅋ|0 +공뭔년놈들이 사기불법탄핵에 주동자중 하나인 이유지.|1 +저시끼들 본색을|1 +그리고 씨발년아 la성경침례교회 김경환 목사님도 |1 +저거 골빈 20대 여자들 풍자한건데 저 캡쳐짤만 보고 욕하는 놈들 수준보소|1 +오 이거 무슨 인스타냐? 뭔데 저런 사진들만 모아져있는거임?|0 +무기징역으루 깜빵가면 완전히 잊혀지기는 할 듯|0 +복지로 임직원카드,카페테리아 포인트 매달 10마넌,사내이자 1프로 대출, 본인부담 실비보험 전액지원, 자녀대학등록금전액지원, 경조사비, 장례비용까지 전액지원, 콘도 리조트 이용시회사부담 60프로, 제주도여행시 차량무료렌트, 메달 영화표 2장, 통신비 지원 등등등 복지는 이정도?|0 +테일윈드입니다!|0 +애초에 제작진에서 맹승지 캐릭터를 떼쟁이 ㅈ폐급으로 작정하고 잡았네 소속사에서도 준비물 그렇게 해오라고 주문한 듯... 진사출연 전에 한창 포텐 터진 유망주였는데 안타깝네|1 +김제동이 저 분야에서는 최고 권위자 아닌가? ㅋㅋㅋ|0 +ㅋㅋ 좌빨들 만선이겠구나 홍어가 만선이에요 만선|1 +무명시절에 유두노출한거 찍은거 본거같은데|0 + 시스템을 짜야한다는 건 누구라도 알고 있는 뻔한 답에 지나지 않는데도 저 개새끼가 꾸역꾸역 반복하는 것은 다음 명제를 위한거다.|1 +공부못함=도박쟁이 라고 또 꼬투리잡아서 욕먹엇을듯|0 +그들은 당연히 사유재산을 가지고 있었지 |0 +빤스 사랑교회|1 +경찰들 일선에서 바로 선긋기 가능... 니들 이제 좃 된거야...|1 +^^7|0 +와 컨셉아니면 자살해라 게이야 시발 ㅋㅋㅋ 이게 사람새끼냐|1 + 흔히말하는 사회통념상 많이 오바하는거임|0 +머리에 대한민국 국기 그려진 모자 쓰고 다니는 노망한 할베들아|1 +이미했지만 이거보고 더 깔끔하게 정비함 슬라이드당 하나씩 녹음기능가져다놓고 바로 보이도록 잡앱들 다지움|0 +ㅋㅋㅋㅋㅋㅋㅋ ㅅㅂ|1 +난 그냥 배달의민족에서 배달비 안 받는 곳에서만 주문 시킨다 ㅋㅋㅋ 치킨,중국집,똥집,보쌈,족발,피자 이렇게 다 즐찾 해놨음 ㅋㅋ 특히 중국집은 요즘 기준으로 진짜 혜자임 홀까지 있는곳인데 최소주문금액 만원임 ㅋㅋ 짜장면 한그릇 5천원인데 두그릇시키면 배달료도 없고 둘이서 한끼 해결 ㅋㅋ 좋음 ㅋㅋ |0 +그런 정상적인 판단을 하는것|0 +그래도 보통 낙태시키지않냐? ㄷㄷㄷㄷ 그걸 책임지네 ㄷㄷㄷㄷㄷ|0 +다른 개잖아. 코주변 털색 비교해봐라 다른 개임|0 +씨발 ㅋㅋㅋㅋ결국 명령불복종으로 사형당할수도 있다는 말이잔아|1 +바퀴벌레새끼들 ㅋㅋ|1 +본인프로 모니터링 안하는애들도많은데|0 +타죽었으면 좋겠다|1 + 시발 그럼 애초에 세계의 좋은 바다를 추천해달라든가|1 +명기에서 부터 그냥 내림... ㅁㅈㅎ|0 +사장님 여기 월급루팡이있어요|0 +얼마 안남았다3년 뒤부터는 아빠같은거 없어도 전혀 지장없이 잘산다|1 +잘뒤짐ㅋㅋㅋㅋㅋㅋㅋㅋ병신련ㅋㅋㅋㅋㅋㅋ지옥으로 꺼져!!!!|1 +그 조사하던 경찰한태 따지든가 막가파는 뒤질때까지 막나갔는데|1 +무식해서 그렇거나 먹고 살려고 그러니 니가 이해해라.|1 +좆 잡고 있었노|1 +기술협력도 해보겠다고 나대다가 미국에게 경고 처먹고 버로우 탔던 게 한국 놈들임.|1 +돈이없어서 좋은데 못살지 병신새끼야 ㅋㅋㄱ|1 +각 집단마다 저런 쓰레기들 존나 많음.|1 +너를 청소해라 안착해져서 ㅁㅈㅎ|0 +.|0 +딴거하다가 좆망했겠지|1 +트럼프 존나젊어보이네 나홀로집에2 나오던때냐|1 +두달 월급만 투자해라|0 +여기서 중,고교 동문 만나네 ^^;|0 +절대선은 아니지만 적어도 국제사회의 룰이지|0 +첸이 결혼한다자나|0 +우리 미애는 못말려|0 +너가 먄만해서 그래|0 +그래서 내가 ㅇㅂ줌|0 +지들 생각이랑 조금만 다른거같은면 반대편으로 매도하고 그러는거 니네가싫어하는 토착왜구가 하는거 아니냐??|0 +역시 브레이킹은 참 어렵고도 심오한 영역이네예 *.*;;|0 +돈 많이 들어가는 병 나면 큰 의료비가 나가지|0 +너 낚지 모르냐? 에구 엠생새끼 라면이나 많이 먹어라 끼니 거르지 말구|1 +짱깨 새끼들은 핵실험 장소 가서 존나게 잘 뛰놀더만|1 +며칠이 맞아요.;;|0 +대구스럽다..|0 +전형적인 좌빨 보지식 논리네 ㅋㅋ|1 +김무성이라니까 김무성중국간첩 놈 김무성을 제거 해야지|1 +팩트 : 어차피 저 여자들도 베충이들 거들떠도 안봄 |1 +배승희라고 선동하던놈 나오시오. |0 + 저 이후에 공개사과함|0 +2년보유면 배당 3퍼센트 배당 두번 플러스|0 + 가장 기본적인건 정규직 직장을 잡고 모은 돈과 주담대로 향후 우상향 가능성이 높은 아파트를 구매하는거라 생각해|0 +꼭 공지영씨 같은 현모양처 만나길 빈다!|0 +진짜 개빡치는............|1 +전염병초기때 사이토카인 이야기하더만 현실이돼셰|0 +원룸녀+자취+헬겔러=200000% 확률로 걸레|1 +누나 찌찌...나 죽어|1 +아니랑께 자발적으로 간 사람도 있지만 아버지손에 이끌려 간 사람도 있어서 ㅁㅈㅎ|0 +복싱스킬은 이미 더이상 새로운기술도 훈련도 없음 그냥 재능충들끼리 경쟁인데 mma는 기술수준차이 씹넘사에|1 + 보는순간 입으로 똥놈어 오겠다.|1 +블라 ㅅㄱ|0 + 한국에 아파치가 36대 있음 한번에 북괴 기갑전력은 다 날아감|1 +첫짤 조국이 기자 보는거노|0 +재건축하면서 바뀐거지.|0 +진짜 의술은 위대해|0 +인체의 신비 ㅇㅂ|0 +본인이 좋다는데 왜..|0 +제들 커서 한국들어온다 |0 +좃망 가즈아~~~~|1 +철부지 둘이 결혼하니 이렇지.. 쯧 애를 낳아도 애구만 |1 +아이들이 안심하고 자랄수 있겠어요?|0 + 이런 연구결과도 있다|0 +불법놈이랑 박았어야지|1 +남자를 지칭해서 용접배우라는거냐?|0 +저 사람이 어떤 길이 편한 길인지 모르겠냐?|0 +ㄷㄷㄷ 최강욱?? ㄷㄷㄷㄷㄷㄷㄷㄷ|0 +그냥 찐따잠바 아니냐? 저걸 유행이라고 또 입는 애들은 ㅉㅉ 개성없는 놈들|1 +아니지 그냥 똥꼬충을 혐오 하는것 뿐이지|1 +댕댕이 표정봐라 구해달라는 표정이다 주인한태 문자 보내고 목줄 좀 풀어주고 집으로 댈고가서 주인오면 보내라 그래도 안오면 유기견 보호소로 보내고|0 +그리즈만 닮은 애네. 엑소도 활동할만큼 했으니 미련 없을듯|0 + 그럼 너 지금 내가 이석기가 빨갱이라고 처음부터 말안해서 삐진거임?|1 +난 이 형 좋아|0 +응.|0 +그딴 소리하는 거보니 예과충인가보네|1 +빠는 사람도 여자 까는 사람도 여자 ㅋㅋㅋ|1 +맞는말했구만 근데 용접은 뭐 쉬운줄아나? 7등급이하 머가리가 용접하면 파이프터져 철아작나고 ㅋㅋㅋ글고 말은똑바로 하자 7등급 이하 머가리가 먼수로 호주가서 영어하냐? 바디토킹으로??|1 +아휴 방구석 딸중독 새끼 마! 거 매직데이 온거데이|1 +감바아 땟목함모는 언제 출격하노 그게 더무섭다 이기야~|0 +판교 잠실은 넣자 솔직히|0 +학대해석하지 마라|0 +한국말하는거ㅇㅇ|0 +대충 갑을관계라는 뜻 같은데?|0 +배달충들 사고나서ㅗ 뒤지란 글인건다 .|0 +마춤법 틀릴수도 잇지 게세끼야|1 +얼굴은 송가인|0 +뭐 어쩌라고?하찮은 축생한테까지 나눠줄 감성따위 없다.|0 +전라도 피해자는 없다는 점애시당초 전라도 사람들은 선행이나 기부를 안한다는 점|0 +백선엽vs김구누가 더 존경받아야하냐?|0 +대체 인강 강사가 사회와 인류에 공헌하는게 뭘까....|0 +존나 무섭내|1 +전차장 커엽노 ㅋㅋㅋㅋㅋㅋㅋ|0 +세상이 바뀌면 거기에 맞춰서 기술도 발전해야지손기술은 필수고 그라운드 기술을 쓰지 않더라도 방어하는 방법을 연구해야하지 않을까?....|0 +게이야..전기차 배터리는 통으로 나오는게 아니라 셀방식으로 aa건전지 만한게 수천개가 깔리는거임...|0 +근데 그 감정의선을 건드렸다는거야 |0 +상류층 개보지랑 하층민 처녀랑 비견되는 현실 자체가 처녀의 가치가 매우 크다는 방증임.|1 +온 나라가 미친사끼들로 덮였네.|1 +그럼 헤어지면 되는 거야. 이해 못하겠음 안 보면 되지. 남의 영역에서 함부러 저 딴 짓거리하는 여자랑 어떻게 사냐. 생각만으로도 숨막힌다|0 +어디서 사회주의 미화질이야? 대깨문새끼들은 일베좀 오지마아주속이 답답하다|1 +이 시대의 악|0 +UFC도 약물시대 주도산이나 케인 벨라케즈때 엄청났잖아|0 +강남은 배달료 넘 비싸 4.5~5.5도 많음;그래도 중국집은 처넌이더라 없는데도 있고 아 근데 어디였지 배달료 천원이였는데 포함해서 미리 카드결제 했는데 와서 배달료 천원이라고 하는 사람 있길래 줬는데 생각해보니 뭔 경우지 싶네 |0 +극혐이네 ㅋㅋ|1 +저건 미국 동남부 앨라베마에서 만든 현기차 아니노|0 +어렸을때부터 금천구 시흥동에 살았는데 나름 서울산다고 자부심같은거 갖고있었음. 근데 아파트 재개발해서 밑쪽 안양으로 왔는데 훨씬 살기좋더라ㅋㅋ ㄹㅇ 금천구는 서울의 할렘가임.|0 +동생이 더 좋아하는것같은데 ㅠㅠ|0 +그럼 우리나라도 타격 많겠네|0 +양육비로 생계 유지하면서 나이트에서 남자 데려와서 떡침|1 +시발새끼 남의 유골은 왜 버리나? 이 새끼 완전 또라이네.|1 +버닝썬에서?ㄷㄷ|0 + 느그들 임마 신종플루 걸리면 한약으로 치료하냐?|1 + 개소리하면서 도망가지 말고 근거 가져와 씨발 새끼야 ㅋㅋㅋㅋㅋㅎㅋㅋㅋㅋㅋㅋㅋㅋ|1 +셀프컷 5년차다. 목말라서 우물팠다. 머리자르는거 생각보다 쉽다. 1년에 한번정도 여자의 손길이 그리울때만 미용실간다.|0 +어렸을땐 홍콩영화 좋아했었는데|0 +일베충하는거보다 나음 유익하게삶|0 +다 거르고 자궁문신만 아니면 됨자궁문신은 그냥 자신이 걸레 of 걸레 인증하는거 |1 +좌파는 입만번지르르하구 실상은 빈깡통이라구|1 +개쫄리나보네ㅋㅋㅋ 잘가라^^|1 + 뇌 안돌아가노 게이? ㅋㅋㅋㅋㅋㅋㅋㅋ|1 +교회놈인감??/|0 +찢어지긴 뭘안찢어 훈련소 2주차때 존나뛰어댕기다가 찢어짐|1 +너똥구멍에너으면타이트해?..|1 +토니 스콧|0 +시발ㅋㅋㅋㅋㅋㅋㅋ잠 못자겠네ㅋㅋㅋ|1 +미카엘은 어감 괜찮은데 영어식 마이클은 뭔가 좀 없어보임|0 +광주에 내려가 변호사 사무실 개업하겠네 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +저 아재는 중금속 처먹어서 저지랄 남.|1 +저나라는 상위5프로 천재들이 이끄는거야|0 +니애미 개보지년 ㅋㅋㅋㅋㅋ|1 +내가 볼때 40 초반까지는 직장 + 외모 맞고, 40중반 넘어야 슬슬 외모 이런거보다 건강 + 돈이지. 돈 없으면 아프지도 못한다.|0 +김지영년들 지들이 존나 어린줄 알더라|1 + 하다못해 저게 식약처 공식 마스코트가 된 것도 아니고 일회용 홍보물에 대놓고 도라에몽으로 쓰인건데 그게 상품화라 주장하는 병신잼.|1 +영입추천한 새끼나 검증한새끼나 들어온년이나 전부 병신같다|1 +여자가별로였나?|1 +이쁘시다 ㅎㅎㅎ |0 +아 ㄹㅇ 그렇게교육함?|0 +저 정도 위치 되는 사람이 그랬으면 발광할걸?특히 교육관련자가 저런 말 하면 걔들도 발광할 것 같음유명 강가 "에이 한의학과 갈 바에 일 년 재수해서 의대가세요" 하면 ㄹㅇ|0 +나도 2번밖에못봤다 먼발치에서|0 +무적권 쌩까라. 쌩까고 절대 연락하지 말고. 먼저 연락올 것이다|0 +손가락 냄새 덜 베길라고 저러나 ...|0 +다 이겼다고 생각했는데 한 번에 판세를 뒤집어 엎는 조커카드 같은 것....|0 +애 낳아서 군대 안간다. ? 출산 안하는 여자는 군대 가야 한다는 말 ㅋㅋㅋ|0 +전한길 아니다걔는 이미 유명해서 다 아는데 뭐하러 말해|0 +흙수저들에겐 이슬람은 극악이야보지 돈주고 사먹는것도 힘들고 인터넷 티비 통제당하고 여고딩 교복 눈요기도 못함술 담배 도박 게임 뭐 하나 맘대로 할 수 있는게 없음결정적으로 돼지고기도 못먹음|0 +동일전과 5범 이상임 무기나 사형가야함|0 +후메|0 +ㅋㅋ취업사기단주제에|0 +인사는 무슨 인상만 존나 쓰는데|1 +또 개씹창내서 마무리 한거야 ㅋㅋㅋㅋㅋㅋ |1 +자랑아니다 게이야..내가 글 싸지른곳이 일베밖에없다 미안하다|0 +나는 저 안에 있는 사람 몇살인지가 제일 궁금함.. 그리고 목소리가 특이한거보면 성우인가?|0 +재업주화...도둑질은 맞음|0 +파충류|0 +이런글쓰는애들 특징 키만큰 씹여드름안경충 대두열폭들 키작은존잘남보면 내보다작내하면서 어깨힘드감 현실은 ...|1 +롱패딩 브랜드 떠나서 다 병신같음|1 +과거로 돌아가서 그래봤자평행 우주라서지금의 넌 그대로이니 아무 변화없어. 헛생각 말아 ㅋㅋㅋ|0 +그게 문제가 아니라 비웃은게 문제자나 ㅋㅋㅋ|0 +대검이 윤짜장 장모 대변인 겸 변호사인 모양이군|0 +기술자들 다 일본으로 떠나서 일본의 정교하고 세밀한 기술력의 원천이 되었지|0 +지옥간박정희만찾는 박무새듳보다는 똥만찬신념이 나을듯|1 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +국경차단이 답이 아니라는 증거가 이탈리아죠|0 + 그리고 프리미엄 결제를 몇명이나 할까?|0 +ㄴㄴ 영화계에선 타율로봄. 원빈은 나온영화 다 대박쳤고 다 연기력 좋게 평가받음 장동건은 친구 태극기 빼고 다좆망. 원빈은 태극기 아저씨 포함 나온건 다 흥. 이 차이야. 영화계에선 그래서 원빈 >>>>>>>>> 장동건 이렇게봄|1 +자성회(自省會) 또는 자생단은 1919년 4월 6일부터 12월까지 활동한 일제 강점기 조선인 시민단체 또는 자치단체의 하나로, 3·1 만세 운동을 자제 내지는 진압, 시위 참여자를 설득, 귀가시키기 위해 만든 조직이다. 전라북도지사로 있던 이진호 등이 중심이 되어 결성되었다. 박중양의 자제단이 전국으로 확산되면서 역시 확산되었다. 3·1 만세 운동 진압 목적으로 결성되었으며, 전라북도에서 처음 조직되어 전국으로 확대되었으나 자제단 만큼 세를 확장하지는 못했다. 자제단 만큼 실적을 올리지는 못하였으나 각지에 설립되어 3·1 만세 운동 참여자를 설득, 귀가조치 하거나, 불응하는 자는 지부와 본부를 통해 경찰에 신고하였다. 다른 이름은 자생단이다.[1] |0 + 어떤 병신은 독재를 사회주의로 알더만|1 +그냥 참 스승인거지 공부에 답이 애들에게 다른길을 제시하는것 뿐인데|0 +버티어라 이탈리아...ㅠㅠ|0 +위험성을 따진다면...|0 +ㅇㅂ|0 +저거 맛있는데 왜 그러노. 마트서 파는 고추장 돼지고기 소스 보다 훨 담백하다. 야채참치 소스맛 나고그리 음식을 가리니 키 170도 안돼지 ㅋㅋ본인은 188찍었다 이기야|0 +그렇다고 공부 못하는 년은 용접이나 해라 라고 말 할 정도로 비하받아야 할 이유도 없지|1 +그 짝사랑을 일절 활용할 생각이 없는 느그가 쓸모없는 쓰레기인거임.그 년을 쟁취하고 싶다면 너의 가치를 높여야하는거고 그라믄 노력해야지.가치와 스팩을 높일 생각은 한개도 없으면서 아아 날 몰라주는 개쌍년 사랑한다!! 뭔 쓰레기 짓거리인지|1 +가능|0 +진심 욕부터 나옴.... 쉬팍색희들..|0 + 다수의 피정복민은 백인이엇음|0 +24살인데 다시 고등학교에 들어갔다고?|0 + 게이는 그런점에선 좋겟노 |0 +뭔소리야 이슬람 원리주의자라면 보수라고 생각하는 너가 무식한거지 보수의 근간은 자유민주주의인데 저게 어디가 보수냐ㅋㅋㅋ 오히려 댓통 허수아비 세워놓고 종교지도자가 일당독재 하는 공산주의 시스템인데|1 +조선에서만 당연하게 생각했던거지|0 +머찌시네유~ ㅊㅊ|0 +낯짝 두껍고 남한테 아쉬운 소리 잘 하고 말빨 좀 된다생각하면 해도 돼|0 +킬러조 오래간만에보네ㅋㅋ|0 + 폭도 새끼들답다 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +다 싹둑 자르자아|0 +개독한테쓰면 딱이겠네|1 +?? 업데이트? 업글? 업글은 뭔데?|0 +씨발 브금 들으니깐 유준호 사무라이칼 생각네네 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +ㅋㅋㅋㅋ개십ㅋㅋㅋ|0 + 소수의 기득권층을 제외하고|0 +신호 안받다가 뒤졌음 좋겠냐?|1 +승용이나 개버충이새퀴나 50대50|0 +위즈원 기다리다 죽겟노이기...ㅠ|0 +이딴걸왜퍼오노|0 +석열이 주위 한씨가 더 부각되야됨~|0 +건강하세요|0 +검찰이 똥을 쌌다 라고 해석이 되는군요|0 +학자 출신들은 혼자서 연구나 해야지|0 + 고덕 신축에 7억8천 분양가 들어갔고|0 +쟤 인스타에서 그대로 퍼왔구만|0 +나 자유우파 맞는데 병신새끼야? 자유지상주의자 리버테리언이야 개새꺄, 니가 우파를 알어? 우파가 제일 싫어하는 게 PC가지고 표현의 자유 침해하고 차별이네 뭐네 개지랄병하는거야 씨발련아|1 +아니랜다 글 내려줘라|0 +42다ㅋㅋㅋㅋㅋㅋㅋㅋ 우연치곤 넘 젖절한데요ㅋㅋㅋㅋㅋ|0 +본인이 좋아하고 동경하는 사람을 걱정하고 생각해주는게 딱히 이상할게 있나 싶음 펨창들이 우리형 커리어 걱정하는게 돈때문에 걱정해주는건 아니자너 괜히 연예인들이 공황장애오고 우울증 많이 걸리겠음 돈이 전부는 아님|0 +이건 뭔 듣보잡이 나와서 개소리야.|1 +엌ㅋㅋㅋ|0 +쪽바리앞잡이야 너거나라가라 꺼져|1 +넌 나의 비올레타|0 +북한남자 10에 9은 동성색스 해봤을거다|1 + 김밥은 어느 나라 음식이노? ㅋㅋㅋㅋ 이 씹새끼야 ㅋㅋㅋㅋㅋㅋㅋㅋ|1 +게이야 고려시대는 꽤 괜찮은 중세시대였다~그리고 조선도 태종 까지는 괜찮은 편이었는데 국뽕들이 물고 빠는 세종때부터 나라가 망조가 들었지....세종은 양반, 귀족들에게는 성군이었을지는 몰라도 조선을 그당시로 쳐도 한 500년은 후퇴시킨 개막장 병신새끼였다 한글도 원래 한자 독음표기법으로 개발한 것이고, 중국몽 정도가 아니라 아예 나라를 짱깨노예국가로 전락시킨 장본인이며, 백성의 40%를 노비로 만들어 한반도 근대화의 초석을 틀어막은 문재앙과 비견될 만한 천하의 개새끼였다 |1 +안타깝네.|0 +발 닿는 순간 벌림|0 +저 강사도 어떻게 보면 예쁜데어떻게 보면 맹해보이고|0 +니까짓게 이길수나 있노|1 +유튜브 코인 타느라 저딴거 신경쓸 시간 없음|0 +할짓없는 놈들이라고 비난하지마라 ㅋㅋ 니들은 할짓많아서 남의사업장에 일하러간다음 10시간 뼈빠지게일하고 시급 만원받냐? ㅋ|0 +사설모의고사잘밧는데 수능땐 4등급맞음 ㅅ ㅂ 83점..|0 +남측정부는 그렇다치는데 한국사람들이 미국 제국주의라고 하는사람 있긴하냐?븅신좆베새끼들 북조선사이버전사들한테 놀아나는것좀 봐.ㅋㅋㅋㅋㅋㅋ|1 +ㅈㄲ 좌음에선 메인에 띄우지도 않음|0 +광떡이가 되면 알아서들 하이소~|1 +요즘 들어 이혼충 좆병신새끼들이 창피한지를 모르고 나대냐 왜케 ㅋㅋㅋ|1 +어휴 ㅋㅋㅋ 독해하나 못해서 헛소리하노 내가 직업 귀천있다고 한적있냐? 도대체 어느부분이????나역시 공부 못하면 기술배우는게 나쁘지 않다고까지했는데?ㅋㅋㅋㅋ일베같은 인터넷 커뮤니티나 친구들끼리 저런말 하는거면 별 문제안될지 몰라도학생들 다보고 가르치는 강사가 강의중에 저렇게 조롱한거면 생각이 짧은거맞다|1 +좋은말 했지만 더이상 나가지는 말자|0 +똥물에 튀겨 ***|0 +변절자 ㅁㅈㅎ|0 +22,357 동의완료|0 +강원도 산불때도 저랬잖아 헌옷 ㅈㄴ 와가지고 음식이 필요한건데 그거 치우는게 더 일이였지 |1 +보통 부장에서 마무리하고 진급하면 더 빨리 퇴직할 가능성이 높으니깐,|0 +하노이 북부는 잘 몰라도|0 +카더라 ㅁㅈㅎ 팩트일베 뒤졌노|1 +저 새끼가 부산 협객 박현우햄이랑 술집에서 다이다이 뜬다?진짜 정색하고 말하는데 저 새끼 30초 안에 척추 접혀가지고 바닥에 똥싼다|1 +췩 췩!이건 입에서 나오는 소리가 아니다|0 +나베 미친년 이렇게 욕하시면 안됩니다.|1 +의새새끼들 다 기어 쳐나왔네디비자고 새벽같이일어나 일해라 노예놈들아|1 +신천지냐|0 + 은혜복음을 기록한 성경은 로마서 ~ 빌레몬서 까지다. 그 이외에 쓰여진 명령은 교회시대 사람들이 지켜야 할 구절이 아니다.|0 +썰좀 푸러봐|0 + 개나소나 공무원할꺼라는 |0 +애미뒤진 빨간마스크ㅋㅋㅋ 저때 뭐라고 겁먹었는지ㅋㅋ 지금 생각하면 기도안차네|1 +축복일까 불행일까|0 + 그전엔 술먹고 할뻔했던적이나 섹스까진 못가거나 한적있긴한데|1 +교회홍보ㅋㅋㅋ|0 +주한미군도 잘 해결되기를 바래요|0 +일뽕 제대로 뒤틀렸네 공공요금 졸라 높은 곳인데|1 +중국인 마음 잡으려면 금색이나 빨간색을 입었어야지 첫인상에서 가장 비호감을 주는 색상이 노란색이라던데...|0 + 12절을 봐.....글자 그대로 이루어 질테니 |0 +깨진거 맞음ㅋㅋ|0 +예능은 다 쇼야 |0 +근데... 한쿨님... 비아x라... 나.... 로또 관련 엄청 많이 보셨나봐요 0- 중간 중간 광고가... 껴있는게 ㅎㅎ;;|0 +신카이 마코토 재일임新海誠한국이름 신해성모르는사람들이 많은거같더라|0 +와 부럽노..|0 +죽으나 사나 나오라고하나봐요...|0 +그러니까 딸 그만 쳐라|1 +뚜렛이후로 못믿겠다|0 +ㅇㅎ 과거도 현재도 민족의식 애국심 존나 안드는건 마찬가진가지네|1 +병신 금오공대 발린지가 언젠데 ㅉ ㅉ|1 +자기 주제를 알면 1번은 쳐다보지 않지 2 3번 정도가 맘에드는 사람도 있다 물론 난 1번|0 +지잡대라도 가는 게 개돼지가 아니고 뭐냐 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +내가 애기 키워줄수잇어.이혼해라|0 +이희진으로 읽고 들어왔네 허허허|0 +수리능력은 생각보다 많은 전 영역에서 쓰이지|0 + 나는 더이상 우파 아니야 좌파야|0 +야레야레... 악수를 두는군...그것도 말 내뱉은 다음에 애들이 채팅으로 지적하니깐 뒷수습 한거일거 아냐...|0 +강사의 시범|0 +다 냄새나는 씹틀딱들이 지랄염병발광하는거임.지도 뒤지면 제사밥 얻어먹어야한다고 믿는 개병신 뇌가 멈춘 산송장.니들 씨발 노인네새끼들아 곱게 늙고 조용히 뒤져라 제사는 좆같은 병신 문화가 맞으니까 깝치지말고|1 +메가스터디 박승동? 그|0 +개일베잡짓거리하다 니인생종친거다.|1 +16년 1월 임용자는 20년 2월 자동진급인데 보통 저때 임용자중에 자동까지 진급못하는 애들이 많지 않거든 쟤는 승진못하고 후배들이 먼저 경장 승진하는거보고 열받앗나본데 그게 죽을이유까지는 안됐을거고 의문이네|0 +신경쪽에 문제가 생긴 것.|0 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 + 쌀한가마니 들기 버거운데;;|0 +영어준비해서 탈조선해라.|0 +토스에 나오는 수치같은거 믿을만한거임??|0 +난 그저께 파키스탄에서 접속시도 했다고 경고뜨던데 시발 ㅋㅋㅋ 대체 어디서 내 구글 아이디가 털린건지 알 수가 없노|1 +꿀 발라 놓았냔 말 말곤 딱히 할 말이 없네요|0 +사실이면 답이 없다.|0 +시신처리 비용은 유가족 몫으로 하고요. 강력 범죄자 새끼들 인권 같은 좆같은 소리 하지 말고. 집행 합시다.|1 +한국도 그러지않나 ㅋㅋㅋ 평소엔 이름만 부르시는데성까지 같이 부르시면 뭔가 화나신게 분명함 ㄷㄷㄷ|0 +있는데 굳이 들어와서,,,,,,,,|0 +외국은 다른줄 아노|0 + 그리고 5급 진급을 위해서 열심히 일해? 대기업은 안짤릴려고 열심히 일한다. |0 +일베에서 살인자는 존나 빨면서 노가다는 성역이노 엌ㅋㅋㅋㅋㅋㅋ|1 +좆병신들 부페 맛없다고? 니 애미애비가 해주는밥 인증해서 올리고 그지랄 해라 븅신들아 애미가 밥안차려주면 방구석에서 라면이나 끓이고 있을 새끼들이 아가리는 시발|1 +맞아 병신들 딸이나 치노|1 +너 애기 때 자는데 밤에 아빠가 물 마시러 나가다 코 밟았나보다...ㅠㅠ불쌍해서 ㅇㅂ야~|1 +아직도 방독면 앉아쏴에서 내가 옆사로 후임의 표적을 맞춰 넘겼을때 좋아하던 우리중대 간부님을 잊지못하고 그 짜릿함을 잊을수 없다 . |0 +하 틀딱 또 도망감? 이걸 붙이면 이해를 못함 노인들 리얼 고지식ㅋㅋㅋ 타조머가리임 리얼딱딱한게느껴짐|1 +여태껏 선진국이라 생각했었던 나라들이 다들|0 +니만 그러는 정신병자 새키 일베|1 +레드벨벳은 어차피 조이도 페미고, 아이린도 82년생 김지영 인증했는데 뭐. 오염은 이미 예전에 됐지|0 +하여간 틀딱들|1 +대학교수가 한 말이 생각난다뛰어내릴려면 9층 이상에서 뛰어내리라고애매하게 떨어져서 안 죽고경추손상 뇌손상죽는거보다 더 안좋은 상황이 생기는 경우도 있다고|0 +레즈커플이네|0 +문재인 병신새끼 ㅇㅂ|1 +세르지 아센시오 배당률 엘클ㄷㄷ|0 +저거 뭔 뜻이냐? 나 이해못하것다 대가리가 끼어서 안 나온다는 거?|1 +이정도면 존못은 아닌듯|1 +시발 미국이가?|1 +진짜 답이없다 큰일이다|0 +2020년 3월 23일. 일베 박멸의 날이네.|0 +개극호|1 +적당한 선에서 주저 앉혀야 한다 대가리 너무 커지면 주인도 몰라본다.$4,000 선에서 놀도록 조절이 필요하다.|1 +불이익????? 범죄 행위를 했는데 불이익?????? 처벌 아니냐????|0 +할배가 2시간동안 안하고 째고 계시면서 ㅋㅋ 단기기억상실로는 좀 약할텐데 ㅋㅋ 열심히 밀어보시길 앜ㅋㅋ|0 +영웅본색아니라서 ㅁㅈㅎ|0 +니 믿음이 옳은게 맞다그리고 애초에 저 교리자체가 구원파의 구원교리임|0 +검은별이란 인형극이 참 재밌었찌 |0 +국유화한 아랍국가들끼리 생산량을 합의해 국제시장에서 비싸게 받는 가격단합을 하는게|0 + 그냥 좌파로 전향할란다|0 +일본에 사냐?무슨일함?제조업 엔지니어쪽으로 외국계회사 노리고 있는데일본어 못해도 가능? jlpt 3급?이라도 가능?|0 +야래야래도대체 이놈의 인기는 언제 시드나나갈 때마다 피곤하다|0 + 4월 1일 밤에는 같은 곳 소학교사에 방화하였고, 부근의 산위에 있는 군중이 이에 응하여 만세를 외쳤고, 다음날인 2일 밤에는 사방의 산 위 80여개소에 모닥불을 붙여 성대히 폭위를 보였다.|0 +갓 블리스 유에스에이 ㅋㅋㅋ|0 +한민관 존나 뜸금없네ㅠㅋㅋㅋㅋ|1 +지랄하네 그러케 독재로 우덜란드 빼고는 다 빵에쳐노코선|1 +니가 말한 거. 공부는 모든 것의 기본이다를 이해못하는 사람이 있냐? 누구나 알고 있는 기본전제야. 얘기할 필요도 없이 누구나 알고 있는거.|0 +언제샀는듸ㅣ?|0 +허위응모가 만개면 업체 끼면 허위응모가 사라지냐 병신아?|1 + 음식도 빵제과류 외엔 맛도 없드만.|0 +보수원로 '이문열 작가'도 내가 위에 적은 내용으로 인터뷰 했는데^^|0 +나도 대학원은 안가서 잘은 몰랐음...|0 +목포 조폭이랬는데 알고말해.여리긴 시발지사형당해 쳐우는게 여리댄다ㅂㅅ|1 +그게 현실이든 아니든 니가 남녀간 다툼을 얘기했고 난 쌍방폭행이라고 얘기하는데|0 +@봄누리 ㅋㅋㅋㅋㅋ ^^♡|0 +졸리 심심하게 사네|1 +비위 약한 나는 돈주고 먹으라해도 못먹겠다 ㅋㅋㅋ|0 +7년 휴직했는데 연봉만 따져도 2.7억임.|1 +ㅗㅗ 새해복 ㅎㅎ|1 +AKSYAKSY AKSY|0 +3명정도에 그밑 4짱이 이제 4년차임 한마디로 존나 꼬인세대 다른 부서도 마찬가지일거고 다른 사무실도 마찬가지임 그래서 존나 힘듬 병신같은경우도 많고 생각보다 좋진않어|0 +확실?|0 +그래서 생체실험이 없었다는거냐 |0 +글 존나병신같이 쓰네.넌 누구 설득하려고하지마라 밑천 드러남|1 +트럼프가 화나면 FBI 동원해서|0 +https://www.youtube.com/watch?v=EZtENB3qA4Y 6:30 부터봐라|0 +누구는 불법인거 모르고 안하는줄 아나....|0 +아니 니 근거란게 있을꺼 아니야 ㅋㅋㅋㅋㅋㅋㅋㅋ|0 +미친영상 ㄷㄷ|0 + 단언함,물론 30%의 공무원은 필요하긴함|0 +솔직히 존예임ㅋㅋ|0 +조상에게 제를 올린 경우는 없고|0 +속이 다 후련.|0 +전 그시대에 태어나지 않아 숟가락만 들고 다녔네요 그래도 국내에서 세금 꼬박내고 열심히 살고 있습니다|0 +나쁜 넘들.|0 +개무섭네 ㄷㄷㄷ|0 +근데 진짜 배달비 넘 아깝다 ㅆㅂ..처음부터 뜯어가는 거였음 몰라 안 받다가 어느순간 보니 서비스랍시고 처받고있네 개좆같은 새끼들2천원은 돈 아니냐? 조오오오오오오오옷나 아깝다 ㅅㅂ그래서 난 되도록이면 걸어갈 수 있는 거리 아님 치킨 안 시킴 ㅎㅅㅎ|1 +마 김치의 힘이다 이기|0 + 근데 실제 cpu gpu 확인해주는 프로그램으로 센타까보면 1060 8400 막 ㅇㅈㄹ ㅋㅋㅋㅋㅋㅋㅋ|0 +아니야|0 +적어도 대부분의 국가들은 국가 기간산업에 잇어서는 시장경제를 준수하고잇다.|0 + 물병에 소주 넣어서 소맥 말아먹기|0 +쿠마몬 파생 하위호환|0 +이건맞다|0 +달창악플러 연령대가 딱 40대|1 +개독질|1 +그놈들은 좆불쉬위하러감ㅋㅋ|1 +저기씨발 BLDC 모터넣어서 노숙자틀딱 새끼들 모여들면 원격으로 터트리고싶다|1 +벌레들 치료하느라 애쓰시네요.|1 +저거 딱 난데 엌ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +주 1회 출전만 지켜주면 여전히 쓸만함 ㅋㅋ|0 +요시자와 아키호는 몇편이냐|0 +ㄹㅇ 투자 전문가들도 몇천 몇억 벌어도 한순간에 까먹는것도 몇천 몇억임 ㅋㅋㅋㅋ|0 +짠~~하다~~~ 왜그러고사니~~ 쯧쯧쯧|1 +야이 빙신아 천조국은 석유도 나니까 땅이 기름지지 ㅉㅉ|1 +뭐.... 좋지 우리야|0 +좀 더 말조심하며 |0 +핵 마크랑 욱일승천이 없네요 ㅋㅋㅋ|1 +예전에 어머니 건물 1층에서 백반집하던 전라도 아지매 생각난다ㅎ건물 주차장에 냉장고랑 식자재들 다 꺼내서 쓰고 어렵다고 해서 어머니가 사정 계속 봐줬는데 월세 2년치 밀리고 나갈때 원상복구도 안하고 쓰래기 가게 안에 다버리고 지랄남보증금에서 월세 1년치 까고 준다니까 소송건다고 개소리 지르면서 씹지랄함그래서 좆까고 밀린월세 다까고 원래 안받으려고 했던 원복비용도 다까버림.|1 +영점타격 ㅇㅂ|0 +10년전에 릴라가 어딧엇노 병신아|1 +강서의 아픈 손가락ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +애시당초 선관위는 왜 저따위 가짜정당을 허가해줬냔말이다. |0 +불신 천국|0 +ㅠㅠㅠㅠ 예전에 고속도로에서 명절날 경미하게 박은 이후로 뽈아서 크루즈 걸고 흥얼거리면서 옴 무서움|0 +제네시스 이차 옆태를...|0 + sm 이대로가면 답이없을듯|0 +요약: 돈내놔|0 +아!! 저 한국사람이에요 너무 비난하진 말아주세요 ㅠㅠ|0 +어 그러네?|0 +아무리 필리핀 이여도 2천원이면 총이 너무 싸다 했다. |0 +감독봐;;|0 +아멘|0 +30대중반인데요새는 라면쳐묵으면 다음날 몸이무겁거나 머리가무겁거나 얼굴이붓거나 설사를하가나 꼭 뭔가 좃같더라라면끊고, 저녁9시이후 안먹고 , 유산균먹기시작한지 3개월인데 매일몸가볍고 똥설사안하고 존나 신세계임.우울증증상도 사라져사 노무 좋음.게이들아 유산균먹어라 한달로치면 1만원정도밖에 안함|1 +자이 팔아서 그돈으로 금호동 대출끼고 3채사서 |0 +그렇지 조폭다구리는 협동형 스포츠임|1 +내가 뚜렛 주작도 잡아봤는데확실히 이새끼는 주작아니다.|1 +모두 빨리 코로나를 극복하길...|0 +ㅉㅉ 중생아|1 +아빠가 누군데?|0 +니들이 아무리 비하해봤자 존나 잘나가고 있다 정치색은 나도 맘에 안드는데 좆밥들처럼 억지로 깎아내리진말자|1 +존나 웃기넼ㅋㅋㅋㅋㅋㅋㅋㅋ|1 +(주)예수 ㅋㅋㅋㅋㅋ 오랜만에 듣는 소리..주식회사예수...특수사업체라 세금도 면제..|0 +아늑하노|0 +가만보니 이국종 민주당 지지하잖아 앜ㅋㅋㅋㅋㅋ이국종 교수도 보수 논리면 바로 고향 바뀌어버리네ㅋㅋㅋㅋㅋ 보수는 대단해~|0 +아니 진짜 솔직히 말해봐 너 지적 장애있지 ㅋㅋㅋㅋㅋㅋㅋㅋ|1 +다리가 짧으면 골격이라도 두꺼워야 되는데 그것도 아님|0 +지랄하네 븅신 새끼. 그짓거리 제일 잘하는게 일베였는데? 그럼 일베를 떠나 븅시년아|1 +짱개 주작 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +어제 하루종일 속상하고 나니까|0 +그냥 집밥처먹으면 될것을 왜 ㅂㄷㅂㄷ하놐ㅋㅋㅋ|0 +더 없냐? ㅋ|0 +결혼 왜하냐좀 사이 나빠지면 생판 남하고 살아야하는건데|0 +구라때리고 있네 남자구만|1 +좌빨페미들의 검찰개혁---기승전 공수처법조인들의 검찰개혁----검찰의 정치권력으로부터의 독립, 일반시민에 고압적인 검찰문화 개혁|1 +그래서 그걸 위반했다고해서 법적으로 처벌 받지는 않지요. |0 +이미 1인당 최소 수십억은 벌어놨는데 니가 걱정할처지는 아닌듯ㅋㅋ|0 +엄청난 대구시야~ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +자동차 라이센스 때문인가 ㅋㅋ|0 +알부랄|1 +이걸 지금 눈치 챘다고?|0 +주작이다나 자취할때 라면,비빔면만 일주일 내내 먹은적 있는데진심 자다가 복통 존나 심하게 오고 변비에 배변도 좆같이 나옴피부 트러블 나고 머리 푸석푸석하고 몸 씹창남라면 적당히 쳐먹어야 된다의사들도 기피식품이 라면,탄산음료임|1 +네 다음 분탕|0 +조슬짤라라|1 +제발 망해라|1 +당사자들은 기분나빠할만한데 |0 +애미뒤진 흑재앙짱깨콜라보집단ㅋㅋㅋ 지들 빨개벗고 목화따던 면역력이랑 어디 같은줄알아 안경을 뿌셔벌라|1 +으|0 +뭐하러 늘림 내가 꿀부대 나온것도아님 훈련 존나하는 철원 부대인데|1 +살벌하노 ㅋㅋㅋㅋ|0 +사재기? 귀찮아.|0 +나때는 스카이 내지 포항공대 ..진짜 드물게 서강대 까지만 학원 선생 가능 했는데세상에..중앙대 출신이라니|0 +최저임금이 오를수록 일자리도 줄고 노동 시간도 줄임바쁜시간대만 알바 쓰겠지 그러니 총 소득이 줄어드는거|0 +솔버지가 정이 많아ㅠ|0 +품번좀|0 +내 말이 우스워? 혼쭐을 내줘야겠군|0 +하...짱깨야 보고있나??|1 +니가 한말 보여준게 뭔 돌려해석이얔ㅋㅋㅋㅋ 정신많이나간듯ㅋㅋㅋㅋ|0 +뭐야이건|0 +와..|0 +진짜 너무 치가 떨려서 글이 안써지네요....|0 + 길고양이나 길어야 5년이지|0 +초중고를 대구에서 살았지만..|0 +노가다 김씨들데리고 방송함 뛰어봐라 |0 +안돼.돌아가.|0 +mother land penis house?|0 +지능 떨어지는 개병신 또라이 엠창새끼 블라|1 +티케이에서 탄핵 못막은 책임진다고 전부 불출마하는게 정상 아님? 이회창은 대선자금 터지니까 이회창이 정계은퇴함 그렇게 책임지는 사람 나오고 천막치고 용서해달라니까 총선 안망하는거|0 + 사회주의 너도 설마 몇몇새끼들처럼 모르냐?|1 +캬 24렙 날라가면...|0 + 천원 이천원이 니한텐 무지 큰돈인갑네 ㅋㅋㅋㅋㅋㅋㅋㅋ|0 +같은 익산 시민으로써 오지랖을 부려봤습니다.^^|0 +아무리 짜증나는 년도 보지에 좆 꼽고 미친듯이 흔들어서 이소령 괴성지르면서 질내사정으로 홍콩가면 사랑스러운 마음이 드는게 사나이의 마음 아니노?|1 +그래도 이번 감독이 저 년 병풍처럼 거의 지워버림|1 +지랄하네무슨구미에 전라도가있노|1 +근데 한녀들은 대부분 저런게 머리속에 있어|1 +위에 있듬..|0 +제일 심한게 유통.제과쪽임...일본과자 아이스크림 조난 배낌롯데나 동아오츠카야 사실상 같은기업이나 제 휴맺은거니꺼 그려러니해도gs25에서조차도 일본식 둥그란 삼각김밥 출시하는거보고 기겁함 ㅅㅂ...뭐 일본에서도 요세 편의점마다 신라면팔고 붉닭복음면팔고 케이팝 여기저기서도 듣린다만한국이 일본영향받은거에 비하면 아직 너무 부족함5년전에 일본애들이랑 이미트 쇼핑가니까 일본애들이 마트물건들보고전부다 자기들 따라햇다고 비웃더라 ㅅㅂ|1 + 한의학은 수능 2등급이다|0 +형 나도 열흘전에 약혼녀랑 그 따-알 다녀옴ㅋㅋ 거 뭐냐 안토니오 모시기에서 아침도 묵묵하고ㅋㅋ 물론 호구 일게이가 쏨ㅋㅋㅋ 그때까지만해도 터지니 뭐니 말 없었는뎈ㅋㅋㅋ 나도 3월에 이주하러 갑니다ㅋㅋㅋ|1 +거긴 눈치안보고 바둑이 풀가동임ㅋㅋㅋㅋㅋㅋㅋㅋ새벽 4 5시에도 추천수가 10단위로 막오름 ㅋㅋㅋㅋㅋㅋ|0 +이제 교회나가자 게이야|1 +심하당|0 +이미 미국은 알고 있던 내용인데 북괴칠려고 명분을 만드는거 같기도..|1 + 그나마 잘나간다 하는 애들은 정자역 ㅋ|0 +5만원어치 보쌈시켜도 저거두배는 되겠네ㅋㅋ|0 +게이야 용기사면 드래곤 사역하고있노?|1 +저런 협회도 있었노ㅋㅋ|0 +진짜 사람새끼들 아니네 ㅡㅡ|1 +스팀해킹방지|0 +응 근데 ㅅㅂ 고졸 무스펙인데 따도 할수있으려나 모르겠다 다시 노가다나 해야하나|1 +대구.경북.포항은 일베틀딱병신들만 있어서 당선됨|1 +사법부가 바뀌어야 함|0 +힘내시길|0 +어디 좆소에서 땜빵조지는 새끼들아닌이상에야 기술이 될려면 대가리가 굴러가야한다 7등급 뭐.. 용접하는 사람들이 수능공부는 별달리 안한 사람이야 많지만 대갈빡이 안돌아가면 용접 잘 못한다 기술이란게 그렇다 대부분 하여간 남 무시하는 저런 발언은 안좋다 기술이 어디 쉽나 어우 배워보면 좆빡세 |1 +나 올해 정시 수능원서 썼는데 훈수두노|0 +25회장 잘생김?|0 +2중대 역할이라도 제대로 하지... 에휴|1 +질타 하고싶다..|0 +당연히 알고잇겠지|0 +장염은 원래 몸살증상을 동반해|0 +이거 니잖아. 이따위로 니 유튜브 홍보하고싶냐? 어떤 놈이 저 댓글 하나보고 이렇게 일일이 캡쳐해서 올리겠냐? 광고하려는게 아닌이상.|1 +언제한번 시간내서 그때 썰 일베에 써봐야겠다|0 +조선족 분탕 중국 마오쩌둥 ㅈ병신에다가 중국 민족이 미개해서 미국한테는 ㅈ털리지|1 +그렇게 하면 안된다.|0 +기사에 매니토바주라고 적혀 있네|0 +이미지 안 좋은데 자한당 추천받아서 국회 들어가려는 죄..자한당 욕만 먹이는 꼴..젊은이들 모두 등돌림 ㅅㄱ|0 +오옹 무슨일 하냐?|0 +흙수저 새끼들은 지가 왜 흙수저인지 모름..저렇게 살다가 나중에 돈 없다고 세금 내놓으라고 함. 걍 빈민촌은 쓸어버리는 게 답임. |1 +에도시대 막부가 세운 에도 의학관에서 나온 의학 책, 인중황(人中黄)이다.|0 +맞는데? ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +중국=일본 |0 +계속 부들거려줘~ 정신병신아 ㅋㅋ|1 +퇴출시켜야트로이김치노|0 +걱정되는 부분이 교민을 데려와 증상유무 상관없이 강제격리 2~3주후 입국 하는게 맞는듯 합니다.|0 +모든 문제는 틀니와 좌익에 있다는게 내결론이다|1 +처음 내말이 이석기 욕한거임 빨갱이나 선거때 그짓함|1 +끼리끼리 만난 걸로 알고 있겠습니다^^|0 +밝은 회색이 유행이라던데|0 +딱 보니까 태극기 부대원이네.|0 +칼럼보니까 예전에 본게 생각나네 유나이티드는 에브라에서 반페르시까지 3-4번의 패스로 슈팅까지 가는 기본 훈련을 중요시하게 생각한다고 했었는데그떄당시에도 펠란이 있었고 그 철학은 역시 영감님으로부터 온것이고 솔샤르도 중요하게 생각하는듯|0 +재미있고 없고는 개인의 취향인데 그걸 가지고자기는 재미없는데 평점 좋네 마네 이러고 있어|0 +나이 본다.. 적어도 내가 나온 곳은|0 +우리집냥이 ㅁㅌㅊ?|0 + 그런데 띠용 댁 성함 석자는 중국이름이네요 ㅋㅋ|0 +언론이 대통령을 빨아주는건 바라지도 않는다 |0 +https://www.youtube.com/watch?v=ynWEbxVJZtE세상은 넓고 재능충은 많다|0 +하나 물어보자|0 +부모님 생신이니까 보약한채 지어달라고 문자보내세요....그리 어려운 부탁 아니라고 하면서.....|0 +하긴 그런 댓글이 댓글포텐 간다해서 다수여론은 아니지글마다 댓글분위기 달라지는게 펨코니까|0 +그건 니 망상이고 아이즈원은 국내팬들 개 씹으로 생각도 안했음. |1 +짱개를 대륙이라부르고 이상한브금까지넣어서 ㅁㅈㅎ|1 +애를 가르쳐야지...|0 +요즘 잘하고있는데 시비거냐 그냥 냅둬|0 +네다음용접충 |1 +...|0 +어디서 이래라저래라 명령질이야 씨발년아 좆까|1 + 살생각이 없으보아더만 ㅋㅋ|0 +하긴 한녀가 그렇겠노|1 +쓰레기통에 쓰레기들이 알아서 모여지니 고마울 따름|1 +똥꼬충끼리 해서 지들끼리 병 돌리면 상관없는데 문제는 그새끼들 여자하고 한다. 그리고 그 여자가 다른 일반 남자랑 하면 퍼지는거...똥꼬충들 결혼도 하고 애도 낳음.|1 +아켄힐마핫벨~도시타테유요~|0 +저는 라면을 원래 좋아합니다|0 +어딜가나 또라이들은 있음!|1 +응 너 똥차 꺼져|1 +에고 해외 토픽이네|0 + 글자를 귀로 듣는 저능아 수준 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +역시 경상도 답게전투는 안하고 가오만 존나 잡노|1 +미개한 문화 |1 +지하철 시트에 냉 묻음|0 +주식으로 주는것도 아닌데 뭔 영향 븅신년아|1 +저런것들은... 믹서기에 넣고 돌려야 함|1 +검사 판사들이 쓰레기들인지라|1 +너가 이해해라...|0 +조심해야겠노|0 +당연히 못넘지. 짱깨새끼들이 거지새끼들이라고해도 숫자는 무시못하는데 베트콩새끼들도 사회주의 시장경제인건 짱깨랑 똑같잖아. 그럼 베트콩의 미래는 짱깨랑 비슷하다고 봐야지. 대도시 몇군데만 좀 크고 나머지 지역은 개씹헬이겠네.거기다 짱깨새끼들은 어느정도 발전하고 임금을 끌어올린거라 그동안 발전토대가 어느정도 만들어진건데 베트콩새끼들 임금인상율이 존나 높다며 ? 그럼 굳이 베트콩에 공장지을 이유가 없어지지.|1 +희한하게도 수도권에선 인물이 안나오네ㅋㅋㅋ|0 +어허 팩폭 너무 잘하시네요 더해줘요|0 +티비조선이 존나게 띄워줬지 ㅋ|1 +이거 이자만해도 매년 엄청난 돈이 들어옴|1 + 좋은 날이 있을거다 내인생도!|0 +그거 본 놈들은 눈깔을 파야죠|1 +무식한새끼야 그럼 연봉으로 고쳐|1 +있어요~|0 +됐고 간만에 보는 일베 서열10년 가입 : 회장11년 가입 : 사장12년 가입 : 부장13년 가입 : 차장14년 가입 : 과장15년 가입 : 대리16년 가입 : 사원17년 가입 : 인턴18년 가입 : 면접자19년 가입 : 지원자20년 가입 : 백수|0 +맨날 징벌 때림..|0 +불륜정사갈보네이거로 바꿔라|1 +그냥 채굴선이다 보니 뭐 그래 했겠지만 좀|0 +본인 본가가 문경근처라더만.. 거기서 쉬고있겠지 |0 + <-이게 현실이다. 문재인 정권과 민주당이 욕 ㅈㄴ먹는것은 맞는데ㅋㅋㅋㅋ 문제는 더 개노답이 자유한국당이라고 심판하겠다고 국민이 원기옥 모으고 있는게 현 상황이야...|1 +이제부터 시작 ㅋㅋ|0 +나도 전기차 애매하게 봄. 차라리 수소차가 더 가능성 있음.|0 +아 아내는 남편한테 저래도 되는구나 하고 무의식적으로 받아들이는 거지.|0 +리얼로 성정체성에 혼란느껴서 남자인데 남자좋아하는 게이는 전체 1%쯤 될듯. 근데 인간은 양성애자라고함|0 +여자 애들은 저런 식으로 텃세 부리는 걸 짬밥 많이 먹고 노련한 거라고 이해하고 행동하더라 ㅋㅋㅋ 한심|0 + 교육자가 하면 안될 말이 뭐 있냐? 법으로 정해놨냐?|0 +대학도 나오지않은 노무현을 무시했다 상고출신이라 무시했다 라는 말로|0 +매운 거 위주로 먹으면 고수가 엄청 들어있지 않은 한 입맛 땡길 거임|0 + 그런적없다 구라쳐서 다른게이들 속이기 시도 하시겠재 병신|1 +갠적으로 저선수들 다좋지만 스도겐키, 야마모토 키드 노리후미 얘네가제일 좋았음|0 +주작글에 몰입하노|0 +찌익|0 +벌레놈들 좆됐다|1 +지디게이 : 내가 크루수장인데, 연봉 1억이 넘는데, 30대 후반 밥 잘사주는 오빤데~~ (부들부들)|0 +초밥 무한리필은 갈만하던데 가끔씩가는데가서 스시 ㅈㄴ조지고 콜라먹는맛에감 |1 +그때 갓 스무살이였음 한창연애할때가 고3이였고 왜 쩔쩔맨지 ㅇㅋ?|0 +저런걸 보고도 뭔지도 모를 만큼 관심도 없고|0 +그러게 말이야 대국의 15분의 1도 안되는 조선것들이 뭔 사건 사고가 많이 일어 나는지 저것도 아마 조선에게 배운거여|1 +ㄹㅇ 가방에 초코파이 한박스 뜯어서 넣고다니면 그동네 봊이들 다따먹었다.얼큰한 소고기국물이 특징인 팔도 도시락사발면이면 모녀덮밥도 쌉가능|1 +판매 예상치|0 +삐빅! 지진 7.4가 감지되었습니다|0 +목사들이 신도들 돈 걷어서 계좌이체해줌|0 +음 뭐라는거야 니 댓글 초반부분 읽다가 지진아가 쓴거같아서 그냥 안읽고 블라함 대충 의미없는 욕지거리 싸질러놓은거 같은데 니 면상 알만함|1 +편피노 등장!!!|0 +용접은 아무나 배우나|0 +오늘 저녁 반찬 걱정ㅇ없네|0 +패쇄가 아니고 폐쇄라니까|0 +중국 쇄국하면 가능함중국이 ㅂㅅ이란건 팩트인데 제조업은 규모 속도 놀랍다 한국기준에선 말도안되는 가격에 퀄리티 뽑아냄로봇공장도 아마 중국이 좆나 만들꺼다|1 +할돈이업어ㅋㅋㅋㅋ|0 +찐따 특: 주류까면 있어보이는줄 앎|1 +템 지급받은 뒤에 본인 스펙하고 안맞으면 교환이벤트도 다시 함 ㄱㅆㅅㅌㅊ|0 +기능올림픽에서 입상한 사람은 연금등 여러가지 혜택을 받는다.기능장려법에 따라 1천2백만원의 포상금과 동탑산업훈장이 주어진다.또 같은 분야에서 일할 경우 연금도 탈 수 있다.연금은 처음 연간 1백30만원에서 시작해 매년 최대 1백70만원까지 올라간다.또 대학진학시 수업료와 기성회비가 지급된다.기능사1급 자격이 자동으로 나오고 산업기능요원에 편입돼 군복무가 면제된다.----------------------------------------------용접공도 ㅅㅌㅊ 가 있고 ㅎㅌㅊ 가 있고운동선수도 ㅅㅌㅊ 가 있고 ㅎㅌㅊ 가 있다.연예인들중에서도 잘나가는 새끼들은 1% 다. 어떤 직업이든 역량에 따른 대우를 받는다.직업 자체로 비하받는건 잘못된거다. 아직도 사농공상 시대냐?|0 +바로 짤린다|0 +5.거기가 미국 밤인지 낮인지 인증하라니까 튄거요? 앜ㅋㅋㅋㅋ|0 +누가 죽음?|0 +ㅋㅋㅋㅋㅋㅋ 부모 욕 먹으니 바로 꿈틀대쥬? 거지 논하는데 자칭 미래가 없는 너가 사는 집은 꽤 잘 사나봐? 얼마나 잘 사는지 인증 ㄱㄱ 못 하면 지옥에 있는 자식새끼들 가정교육 잘 못 시킨 뒤진 니 증조할아버지부터 시작해서 싸그리 모가지 잘라버린다?ㅋㅋㅋㅋ 서비스로 애미는 변기통에 머리 쳐넣고 밟아서 목 꺾어 죽여드림|1 +ㅇㅈ 솔직히 틀딱들이 박정희 좋아하고 보수 지지하는것도 무슨 신념이 있어서라기보단 그시대에 박힌 아집을 그대로 가지고있는거에 지나지 않는다 봄|1 +안나 추? |0 +정당방어가 범죄자를 다치지 못하게 규정한게 얼마나 한신한 법인지..애초에 범죄가 없으면 정당방어 할 닐도 없는데..관련법 개정이 필요하죠. 과도하게 정당방어하면 뭐가 문제라구|0 +복근운동 자주하고 따듯한 물 따듯한 음식 먹고 기름진거 가급적피하고|0 +찐따라기보단 유행하는클론옷이지 ㅎㅎ 찐따는 시장표애미가사온패딩입음|1 +집에 움직이는 샌드백 두개있는데 왜사노|0 +근데 그냥 그분들 맥이는거 아니야??보정 어플 쓰는거 보니까 뭐 있을거 같은데|0 +용접이 무슨 앉아서 지지기만 하면 되는 건 줄 아나본뎅, 실상 그게 아니거덩. 응용력이 필요한 거거덩. 그게 기술자거덩! 앉아서 지지기만 하면 노가다! 응용하고 짱구를 굴리면 기술자!|1 + 구약의 율법은 우리가 스스로를 구원 할 수 없는 무능함을 보여주는 척도라는 목적을 달성했다고 한다.|0 +저런 복지정책 때문에 망가진 1인임. 어리석은 선택은 자녀를위해 하지 말기를.|0 + 세상 모든 여자들이 그렇게 날 좋아하는거였구나|0 +총대한번 매라 |0 +초반에 군기잡는거는 아닌거같음 그냥 성격이 존나 이상함|1 +대구 경북이 가슴 아플듯|0 +이야기해서 그담날 사장님 덕에 돈 많이 벌고갑니다 문자 남김.|0 +@삐뽀드리미 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +아베 진실을 말하다|0 +이게 진짜 너무 슬픈 현실이라고 봄|0 +기능사 수준 단순 계산이라도 보통 수학선생이라는 인간들이 공업분야에 수학이 얼마나 다양하게 쓰이는지 그런것도 제대로 아는 새끼들이 있냐 이말이지|1 +주작ㅋ ㅁㅈㅎ|0 +일본얘기하면 부칸같은 쩌리를 들고 나오더라ㅋㅋㅋ|1 +@청풍호지기 |0 +질본이 다하구 가만 있다가 발표한거 가져와서 웅변하는건가?|0 +자동기능만 있으면 갓겜|0 +없는사람 이해해쥐야..|0 +치료 잘 받으시고 건강하게 돌아오세요..ㅜㅜ|0 +조선공산당은 광복 직후인 1946년, 이관술의 지휘하에 위조지폐(당시돈 1200만원 가량)를 찍어 내다가 미군정 경찰에 의해 적발되었다. |0 +응 평생 문 열지마라.. 써글놈들..|1 +ㅇㅈ 다른건 몰라도 유산균만 꾸준히 먹으면됨 |0 +전라도는 인구 소멸 우려될 정도로 인구가 엄청 줄었는데 공무원 10% 증가 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 개같은 사회주의 지상낙원 싸우스 코리아 전라 프라빈스 |1 +우아.. 여그 신고 먹는 벌레들이랑... 싱크로율이 거의 100 에 수렴 하는데...|1 +양세형이ㅋㅋㅋ |0 +부럽다|0 +저러다 ㄹㅇ남자가 바람피면개꿀잼 ㅋㅋ|0 +이새끼는지는안가져오면서 나보고가져오라노 개씨발련이 뒤질라곸ㅋㅋ|1 +진짜 윗댓 말마따나 샘숭 다닐 능력치면 전기기사 이런거 없었을려나 최소 아파트 관리소는 취직했을텐데 얼마나 ㅎㅌㅊ면 협력업체 좆소들 1차 2차 벤더에서도 임원으로 안불러줬노|1 +가입일 얼마 안되는 빨랩은 분탕가능성이 있음|0 +사람들 왤캐 침착하노 ㅋㅋㅋㅋㅋㅋㅋ |0 +진짜 빡대가리들|1 +알겠는데 왜 짤은 19년 초창기 짤이냐? 20년인 지금도 버티고 있어서 ㅁㅈㅎ|0 +거기 혹시 민노총이나 전교조 애들이 운영하는 곳 아니냐??|0 +칼잽이ㅇㅂ|0 +띡 !|0 +제발 대구 젊은이들 이번에야 말로 꼭!!!!!!!!!|0 +젊을때 매춘부짓해서 꿀빨았으면 늘그막에는 조용히 살다가가야지 나이처먹고도 정신을 못차리고 과거를 미화하려하는데 천벌받는다|1 +우리집도 제사 없는데 제사를 꼭 해야 한다면 부모님만 모시는게 낫다는 1인|0 +그러니까 짱깨배달이나 하지ㅉㅉ|1 +반전이라고 해야하나 이것도ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +스포츠 머리 난이도 존내 높다.옆머리 뜨는 것은 다운펌하는게 맞다.셀프 컷 하면 뒷머리 좃같다. |1 +빙신들!!!!!! 저 와꾸에 저 피지컬이면 한국어 좀 배워서 김치만 처먹어도 월 10억은 간단하게 땡기는데!!!!!! 왜 힘들게 몸 파냐 이기야!!!!!!|1 +김선교 저 얼간이는 이제 팽당했으니 막가자는거지 아 한씨였구나 관심이 없어서.....|1 +열받네요 ㅡㅡ|0 +ㄴㄴ 제일 딸리는건 니 아이큐 아님??ㅎ|1 +저 새끼가 정치에 얼마나 관심이 있겠냐|1 +정신병자|1 +저것도 통계마사지 한거라서 실상 더 나쁘겠지 |0 +진짜 만능짤이네|0 +홍성 헬게이트 오픈인가요?|0 +좀 있으면 들개들 3마리 몰려와서 레이드 시작함ㅅㄱ|0 +저러면 첨부터 그만둬야지 왜 인제 말하누....독서실 사장두 못됬지만 저녀석두 왠지 그쪽의 냄새가....|0 +3000이면 데렉 지터 안타하나 칠때마다 떡을 친거네 |0 +계정신고해라|0 +피클되겠노|0 +중국이라고 믿고 싶다|0 + 근데 정규재 신의한수 안정권 신혜식 이런애들 학벌도 고졸에 구리고 베리앤굿 창녀촌이나 운영하던 쌩양아치 죄좀빨갱이 새끼들임 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +정도가 너무심함 저런새끼가 어떻게 사회생활가능한지 의문|1 +돈이 모든걸 해결함|0 + 그냥 자극적인 유튜브판에서 재미가 없고 대체품이 너무 많이때문에 자연스레 도태된것 뿐|0 +무기팔면 파는 나라 입장에선 사가는 나라가 카피할까봐 이런 부분도 우려되는거임?|0 +난 이런 연출된 사진은 노꼴이더라 ㅁㅈㅎ|1 +사이비놈들이 뭐 어쨔??|0 +재수좆나게업는 허접하고,쓰잘데없이 메스컴나와 발공떠는여, 재수업서서 이쌍ㅇ나오면 채널돌려요. 이영자돼지같은ㅇ하고, 재수없어|1 + 난 빨갱이나 하는짓을 닭도 했다는 이 팩트를 니가 자꾸 말하는거 재밋음|1 +돈고 생기고 친구도 얻고 요즘들어 보기힘든 장면인데 ㅊㅋㅊㅋ 입니다|0 +원작이 따로있는데. 일본 애니짤은 왜 올리노 ㅁㅈㅎ 존나 웃기네 짤방은 72년작.흑백시절 부터 영화가 나왔지만 꾸준한 스타일이었음.|1 +상습 주작글 올리는 개새끼야|1 +병신.|1 +이런걸 은사죽음이라고 하는거다~~|0 +개인적으로 페이토자 바다하리 좋아한다다만 바다하리는 네츄럴이아닌 약빨이라서 실망했고페이토자가 경기도 재밌고 멋있는 사내라 젤로 좋아한다일단 공격도 임팩트있고 뭔가 졸라 멋있게싸움ㅋㅋ 극진가라데 기반이라서 타격하는것도 개멋있고경기보면 상대방 ko 시켰을때 하는 피니쉬포즈도졸라멋있다 진짜 엄청난 강자인데 챔피언을 못해서 아쉽다 ㅠ유명한일화중에 하나가 사촌동생인가 뭔가가 억울한 누명으로경찰서에 잡혀갔나 두둘겨맞았나?? 암튼 그런 사건에 휘말렸는데화가나서 혼자서 경찰서 쳐들어감경찰 수십명을 혼자 맨손으로 초박살낸 일화가 유명하다|1 +교타로가 전형적인 인자강 씹재능충임 맷집도 존나쌔서 KO안되고 복싱전향후 귀신같이 동양헤비급챔피언 ㅋㅋ 요즘은 노화와서 세계챔피언전에서 패배|1 +저거랑 살아남기시리즈 학교에 가져오는순간 바로 인싸행|0 +돼지고기 파는곳들 대부분 정육점에서 고기받는게아니라 도매시장에서 받음?|0 +정의당, 전교조가 요즘 고등학생들 열심히 선동 중우파는 철구한테 감사해야 된다|0 +그냥 가서 강간한다음에 깜빵한번 다녀와라요즘 깜빵가면 존나 편하다. 일과시간에 책이나 좀 읽다가저녁ㅇ되면 tv 컴퓨터 플스 다 있는 방에서 혼자 즐기다가 10시에 잠들면 되고하루 세끼 존나 잘나온다|1 +법을 어겨서 화내는게 아니라, 내가 누리지 못하는걸 나 몰래 누리니까 빡치는거임.|1 +일리가 있구나|0 +걍 쭉 그렇게 믿고 살어 빙신아|1 +요로결석이 있으면 물많이 먹어|0 +미안한데 국공립 4년제 나왔고 필드나와보니 졸업장 별루 필요없더라.|0 +6. 그라우베 페이토쟈는"브라질리언 킥"을 만든 (1988년인가 1989년도에 그의 스승격인 프란시스코 휘리오 당시선수가 만들었다고 주장함)애제자로써 프란시스코 휘리오는 재일교포 2세 문장규(마쓰이 쇼케이)와 함께 극진회 슈퍼스타였지.. 어쨋든 스승이 극진쪽에선 적이 없는 괴물 스테미너의 선수였지만 k1에서 성공을 못하자 자신의 제자로 만회하련 느낌이 들었음 (실제 경기에 자주 관전및 코칭스탭으로 나옴) 근데 이때 그의 평생의 악연?인 후배~ 정도회관의 슈퍼스타 쉐미슐트가 전성기때라서 두고두고 (내가 알기론 4번 경기치뤄서 모두 패함) 수년간 큰 아픔을 겪었지. 실제 둘의 경기때에는 극진회 주제가,깃발 나오고 오야마 마스타츠(최영의 초대총재) 이름 나오고 다음에 후배격인 정도회관 주제가와 깃발나오고 이시이 카즈요시(정도회관 초대총재 /실은 요절한 다른 인물이 정도회관 설립에 중추였는데 이름이'":;")나왔던..|0 +논밭대학교|0 +개꿀이라는데 친구두놈 9급충임|1 +웃기고 있네 가세연이 정상이냐 박사모지|1 +보고 느끼는게 없냐|0 +하늘 존나 어두침침해서 기분나쁜데|0 +걍 한국인 특징임. 잘못된거만 알려주면되는데 꼽주고 갈구는거. 글쓴이 미필이냐|1 +래미안도 래미안 나름이지 어느지역에.따라 다르지|0 + 교사한테는 학생이 제자지만|0 +오랜만에보는 조킬러~~|0 +외적인 부분, 모양새로 단정짓는 습성이|0 +ㅇㅂ에선 좌좀분탕이란 오해를 살 수밖에 없음|1 +경찰이 무혐의로 종결후 검찰에 보고안해도 됨. 검찰의 보완수사 요구를 거부할 수 있게 됨. 지금은 검사의 지휘하에서 수사권있음|0 +폰 둘이서 만난것 같노|0 +ㅇㅇ ㄹㅇ임|0 +옛날에 게임하다가 알게된년이랑 만났는데 뚱녀였다 하루 먹고 연락 차단 했는데 자괴감 씹오진다 꼬추자르고 싶을정도로|1 +서울시장이 뭔데 국방까지 관여함 ㅋㅋ|0 +김대중은?|0 + 나만한 세입자없음 씨발|1 + 비번조합만들면 무적임 ㅇㅇ|0 +당신은 따뜻한사람~~~~~~|0 +핵폭발하고 지들좋아하는 개천기로 업글해쥬쇼.|0 +왜 내 인생을 여자한테 쪽 빨리고 사냐?|1 +아줌마가 노가다하면 남자들한테 존나 따임|1 +쟤 말도 틀림. 쟤 말대로 하자면 천한직업 가진 사람도 대접을 받아야 한다는 당위 명제에는 동의하지만 사람의 가치에 귀천이 없다는 명제엔 동의 할 수 없다. 사람에겐 분명히 귀천이 있다. 허구헌날 게임이나하고 술만 처먹으며 인생낭비하는 망나니와 일평생 국가만을 위해 살아온 사람과 가치가 동등할까? 직업에도, 사람에도 귀천이 있다.|0 +그나마 절반은 은혜를 안잊고있자나|0 +근데 니들은 저게 진짜라고 믿는거냐?일베에서 허세부리는 망상충하고 뭐가 다르냐 저게|1 +틀딱드라마 조연으로 많이나옴|1 +일본어 잘하나보네|0 +으따 이 우주도 일본이 창조했당께요?!? 예수님도 알고보니 일본인이셨당깨요?!?|0 +간단하게 말해|0 +공손하노.|0 +감동실화냐 와 선생님들..|0 +지랄똥싸구덜자빠졌네|1 +개 병신 새끼들이 잘해주면 자기 아래로 보고 어떻게든 더 뜯어먹을려고 함진짜 개병신 종특 시발 ㅋㅋ|1 +돈받는게 짱인듯|0 +승제는 까지마라.. 승제없었으면 취직도 결혼도 못했다|0 +저게 리얼리티 프로도 아니고ㅋㅋ 시트콤에서 말한걸 왜 지랄이냐? 미친놈들인가ㅋㅋㅋ라고 생각했는데김민교도 저 방송에서 참전용사 할배 연기했다가 직잡찾아가서 사과까지함ㅋㅋ그리고 저 프로 기획한 피디는 짤렸음.|1 +달게 받자 ☺️|0 +모두가 비슷하게 생각해도 입 밖에 내면 안 되는 말이 있는 건데... 용접'이나' 하라고 했다면 언어 선택에서 화자 본인의 가치평가가 들어가 있다고 판단해야지. 그러면 욕먹어도 싸다. 본인이 본인 말에서 용접을 평가절하 했으니까.|0 +ㅋㅋㅋㅋㅋㅋㅋ원하는대로 ㅋㅋㅋ|0 +편의점주?|0 +줄서봄니다~|0 +게기판 옵션 넣으라는거지...|0 +연기에 냄새에 |0 +벌레들 ㅉㅉ|1 +야사올렸다고 강간당하고 자살당할 이유는 없는거임|1 +짱개새끼를 존나게 빨아대네.|1 +이사진은 선거끝날때까지 계속올려주시기바랍니다|0 +빠는거 많이 찍음|0 +앞에 어머니시냐? 가능하다고 전해드려라 |0 +골프는 안좋아했나보네 ㅋ|0 +@봉봉주세효 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +이기 뭐라꼬 빵터지노|0 +애초에 왜 싸우냐? a라고 했다가 b라고 한들 그게 머가 어떻냐? 그냥 그래? 알았어 그건 그렇고 섹스나하자 하면 서로가 편 할 일 아니냐어차피 가진거 하나 없이 더 이상 잃을 것도 없는 놈이 a면 어떻고 b면 어떠냐??인생 좆도 없어 그냥 섹스나 하면서 쾌락 느끼고 살다가 틀니차고 있다가 노짱 보러가면 되는 거야내일 가서 무릎 꿇고 존나 빌고 다시는 그러면 손모가지 자른다고 하고맥주에다가 치킨 시켜서 먹고난다음에 물고빨고 섹스하고 나면 앙금은 있겠지만 언제 그랬냐는 듯이 또 봉합됨그것이 부부관계임딸치지말고 모아서 중요할때 사용하라 이 말이야|1 +저도 그 부분에서 울었어요|0 +네다통|1 +한줄기 빛 드립은 인정한다. |0 +그건 개인간 관계에서 그렇고..|0 +옛날 아파트들 현관입구에 툭 튀어나온 거기에 독 둔다는것같음|0 +똑같이 해줘라. 쫌|0 +아하부장을 펨코에서보네 ㅋㅋ 레시피 개좋음 ㄹㅇ 실패한적 단한번도없다|0 +이 땅에 살던 모든 사람들이 부활하여 하나님께 심판을 받을 것이다.|0 +자지를 용접해버렸네|1 +잘하는군요 잘해요.|0 +못생겨도 자기관리를 하면 기존보다는 나아집니다.|0 +그냥 횡보 중. 9억 이하는 몇달 후에 여파가 갈것임.|0 +캬.....너무 잘봤습니다 밖이라섯 유툽은 집에서 볼께욧 일단 ㅊㅊ!6|0 +울산게이ㅎㅇㅋㅋㅋ|1 +ㅋㅋㅋㅋㅋㅋㅋㄱ 사실상 인스티즈랑 다를게 없노|0 +오픈카 이런 개념인가?|0 +일베 인기스타냐 ㅋㅋㅋ|0 +제목 읽기 개힘드네일단 ㅁㅈㅎ줌|1 +너 맨날올려서 이런거 척척박사잖어 ㅋㅋㅋ 이색기진짜|1 +강릉에서 도배알바 부사수가 일급 20땡긴다|0 +어떤 놈는 선거에 맞춰서 남북정상회담도 조작질 해대는데 어떤 년은 선거가 코앞인데 담배값을 두배나 쳐올리고 자빠짐 진짜 미친 또라이년|1 +니가 억울하면|0 +익명 게시판은 거르는게 답|0 +내가 아는건 그리즈만 에메르송 등등|0 +이 반일국뽕 개색기들은 일본을 배제하고 미국만 따르면 된다는 개소리를 하고 앉아있음.|1 +이럴땐 사이다 럼프형|0 +헉! 조선족!|0 +강간범은 뒷구멍 개통으로 정의구현 웃흥웃흥|1 +지금이랑 다를바 없네|0 +그게 바로 니가 프레임에 걸린거임 ㅂㅅ아 ㅋㅋ|1 +부랄탁!|1 +조센도 만만찮아얼마전에 일가족 모두가 보험사기쳐 보험금6억을삥땅한 일도 있었는 데 저건 암껏도 아님보험사기를 중형으로 다스리는 선진국에 비해솜방망이만 휘두르는 개판判 부터 조져야 함 |1 +질본대응은 백점만점|0 +심정은 이해가고 첫줄도 공감하는데같은 남자로써이러는건 겁나 이해안됨ㅋㅋ 세상에 미친짓거리한 남자가 한둘이아닌데 왜 그걸 같은 남자라고 부끄러워해야하는지|1 +AV도 가능할려나 ㅋㅋ|1 +80퍼나 90퍼나 에휴;|0 +QE는, 매번 남중국해 온다고 하는데, 왜 진짜로 오지는 않노.|0 +제발 아멘|0 +애미로드 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +필리핀이면 돈백이면 해결되겠네요|0 + 요즘은 남자도 겉눈썹 정리한다고 하던데|0 +찾아본다고 노력한건 좋은데 장묘시설내에서 처리가 끝나면 폐기물이 아닌게 맞음 ㅇㅇ근데 처리를 끝낸게 아니라 화장 후의 부산물을 갖고 나와서 집에 뒀자나?그건 폐기물이 맞고그걸 도로 장묘시설에 가져가서 납골당 같은곳에 보관하면 괜찮은데그냥 뒷마당에 뿌렸자나??? 이건 폐기물 관리법 위반이 맞음 ㅇㅇ멍청한놈 ㅋㅋㅋㅋㅋ|1 +ㅋㅋㅋ 저 표정 손모양. |0 +틀무새랑 자한당무새들 싹 사라진 신기한 글이네 ㄷㄷㄷ|1 +백퍼센트 일주일도 못치고 창고행ㅋㅋㅋ|0 +이제 헤루조센에서는 경찰이 체고조넘이시다 머리를 조아려라|0 +난 모유물|1 +니땜에 더 깜 ㅅㄱ|0 +이새키는 관상은 과학이 아닌것 같기도 한데ㅣ 하는짓이 503, 아베,나베 하는짓과 비슷함|1 +복싱 배워라 원투 펀치로 쉽게 제압가능 |0 +우리는 이미 너희 쪽본을 지도에서 지웠다 |0 +그리고 독립군을 도왔습니다... 교포도 대한민국 사람입니다 ㅠㅠ|0 +우하하하~~ 건투를 빈다.|0 +잘됐네 빈자리에 남경이나 다시 채워넣어라|0 +찐따 좆같은 점 :사실 따지고 보면 찐따가 피해준건 없음근데 만인의 적임|1 +저글 니들얘기야.|1 +ㄹㅇ 나는 걍 깔끔한거 좋아해서 공부하러 갈때 제외하고 꾸밈 |0 +노짱 사진 4기가있음|1 +ㅋㅋ ㅇㅈ|0 +훠훠훠 진즁권씨바둑이보내겠슘니돠 컴퓨터확인해보시죠 쪕쪕쪕 |0 +쟤 알바좀 그만풀라해라ㅋㅋㅋㅋㅋㅋ|0 +화산터짐? 심하누?|0 +최근에 본 실사화 중에 젤 잘 만든 거 같네 ㄷㄷ|0 +근데 수도권에 일자리도 몰려있음|0 +슬슬 기어들어 와도 해줄까 말까인데 별 거지국이 깝치고 앉았네|1 +중국여자 떡여행은 괜춘 가슴큼|1 +정신적 피해보상 소송준비 하시는듯 ㅋㅋㅋㅋㅋ진짜 이런선비같은 아재한테 7등급 드립이나치는 쌍년 ㅋ|1 +어디서 근무 하는데?|0 +그런 의미에서 밥이나 사라|0 +방생하지마라....|0 +그렇게 되면 또 품귀현상이 일어날 것이고 뒷켠에서는 또 사재기를 하는 사람들이 생겨나겠죠~|0 +캬.ㅅㅂ니네집에 빈대도사냐 얼마나 ㅈ같이생활하면 그런게 다보이겟노?|1 +흙아니냐|0 +'유쾌하시네'라는 리플에 꽂혀서 ㅈㄴ 웃음 ㅋㅋㅋㅋㅋㅋㅋ|1 +예수는 지구최대의 사기꾼일수도 있음 |0 +전두환 우표가 제일 싸더라|0 +중이 제머리 못깎음|0 +김포는 뭐 김 네장씩 싸먹는 새끼들이냐?|1 +우리때는 늙어도 일 가능하지 586들은 일 못 할듯 거기가 마지노선|0 +짤 ㅇㅂ|0 +그래 씨발년아 내가 저 본문쓴새끼 처지가 하도 딱하고 맘이 아파서 좀 오바했다 |1 +순하고 착하면 남자는 등쳐먹히고 여자는 걸레되기 딱 좋은 듯|1 +저거 포스팅하는 사람이 소개할꺼 넣을꺼 없어서 잡아 넣은거지 뭔 국가기관드립이야.|0 +남자보다 18%만큼 덜 주면 된다. 여러가지 다른 요인도 많지만, 남자근로시간이 여자근로시간보다 더 많거든.|0 +내 또래치고 괜찮다. 아직 꽤 곱다. |0 +성결교가 기독교 3대 종파중에 가장 규모가 작지요|0 +역삼도 원룸사는 창녀들 존나 많다 ㅋㅋ|1 +저 가운데 앉은분이 말하길 여자 질크기는 입크기와 비례한다 하였음 즉 입작은 여자만나라 |1 +나 메이커만 사 입는데 ㅋ메이커 아닌 옷들은 도대체 어디서 사야 되는지도 모르겠다 솔직히|0 +미친년인데 ㅋㅋ|1 +조회수 믿지마라 돈 주면 조작 가능하다 지금 조회수 믿을 수 없다 |0 +오호 이거 맞는 둣|0 +김구라에게 달릴 머리가 165에 달림|0 +인생좆망테크 탓네 어휴|1 + 병신 빡대가리 반일국뽕 수준 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +MBC? ㅁㅈㅎ|0 +그럼 시발년아 니가 잠수한 곳이 이상한 거지|1 +너 틀니 2주 압수|1 + 조만간 민주당 정당가입도하고|0 +넘어가 지금의 대한민국 있는거고 ㅋㅋㅋㅋㅋ |0 +애들아 진지하게 질문하나 하자내가 고기를 존나게 좋아한다 특히 돼지고기,삼겹살을 진짜 삼시세끼 매일 먹고 싶은데 정육점이나 고기집하면 고기 매일 먹을수 있냐? 진짜 진지함 정육점이나 고기집 들어가서 직원으로 일하면 매일 고기 먹냐? 고기 먹는 직업좀 알려줘라 나 그러면 평생 일할수 있음 (돈 많이 벌어서 그돈으로 고기 많이 사먹으란 개씹소리는 하지말고 )|1 +죄인아 너만 부엉이바위가서 뒈져주면 국민대통합된다남북통일도 되고제발 가라 부엉이바위로|1 +현재 모든 헬조선인은 양반 노비 양쪽 dna를 전부 갖고 있음 굳이 양반노비 나눌필요가 없음 노비인구가 압도적으로 많은 나라였으니 노비의 dna를 더많이 가졌겠지 노비종족이라고 봐도 무방함 양반후손입네 하는게 다 지랄육갑떠는거에 불과함|1 +친중이니 친미니 친일이니 ~|1 + 블라한다 ㅂㅂㅂ|0 +이것도 얼굴이 준비물이네|0 +자기 목을 자기 손으로 칼로 썰어내는 판국인데 오천만 개돼지들은 뭘 모르는 거냐?|1 +돈내고 받아 씁생이시키들아 500조|1 +보지오? |0 +꼴리면 정상이냐?|1 +넵|0 +뒷문|0 +조ㅈ만아|1 +모자 개이쁜데 어디꺼예요?|0 +아이들은 선생의 말을 믿고 따르게 되어 있음|0 +좋은자료 ㄱㅅ합니다|0 +말도 안되는 개소리임 ㅋㅋ 컴퓨터쪽 지식 조금만있어도 개소린거암|1 +그리고 들은바로는 PE는 석사이상 되야한다고...|0 +금수저야 동수저야?|0 +씨발 문재앙 때문이냐? 왜 점점 사회가 공산주의화 되가냐?니들 보고 용접공 하려면 할 거냐? 니 자식 새끼 용접공 시키라면 시킬 거냐고사무직과 용접공 둘 다 급여 조건이 똑같다는 가정 하에 용접공 선택하는 사람이 몇이나 될 거 같냐?그게 싫어서 학창시절부터 열심히 공부해서 좋은 성적 받은 사람이 좋은 급여와 조건에서 일하는게 맞지.뭘 자꾸 용접공도 훌륭한 직업이네 뭐네 개소리를 해.물론, 누군가는 반드시 해야하는 일이지만 그게 너라면 , 너에게 그것말고 선택의 폭이 넓다면 안 할 거잖아.왜 자꾸 개씹소리들을 씨부려.공부 못하면 직업 선택의 폭이 좁아진다는 말은 한 건데 왜 이리들 광분함?|1 +사람이나 동물이나 증상나타나면 늦어요.|0 +게다가 도움 주려고 주변 정리 해준 여자랑 고양이 때문에 파혼....... >.< 미챠.....|0 +참 추잡하다 진짜 .. 아들내미 고맙다는 인사도 없이 받아 쳐먹는다는거보니 보니 모전자전이네 .가정교육 존나 중요하다 ㅋㅋㅋㅋㅋㅋㅋ|1 +4.기쁨조 운영|0 +최고다! |0 +내일 얼굴공개 여부가 된다고 하는데 지켜봐야죠|0 +진짜 왜들 문재인에 미쳤냐 못느끼나.,,잘못되가고있다는걸|0 +이제 알았냐? 빠른 이민만이 답이다.애국? 좆까라 그래라.|1 +자기도 도움을 받았으니 그거에 대한 도리를 한듯|0 +나 외국에 있기 때문에 원래부터 스냅임|0 +일본이 규제가 왜 없음ㅋㅋ|0 +서울 7시가 불체자 불법입국자등등 많을수밖ㅇ ㅔ없음 바닷가랑 가깝고 집값도 쌈 서울에 속해있지만 사실상 인천 경기도에 가까운 동네임 그러니 불체 외노자 조선족 짱개등등 온갖 인간들 다 들락날락 함|1 +ㅋㅋㅋㅋ 개인입장에서 업체 통하는 메리트가 뭐임?|0 +는 희망사항이고 중견이라도 들어가면 감지덕지다 ㅠ|0 +철수가 드디어 대가리 철판 깔았네^^|1 +하기사 벌레가 뇌같은게 있을리가...|1 +와 씨 애들 한동안 무서워서 잠도 못잤겠다.|0 +고당 때 좆같이 생긴애 이름이 김종대였는데 ㅋㅋㅋㅋ|1 +코는 성형한 거지?|0 +그게 어디서나온 자료냐?|0 +일게이들이 제일 좋아하는 소재|1 +에고... 세상이 어찌되려구... ㅠ.ㅠ|0 +이게 정답이노|0 +조현같은알바가 고기집 서빙하면 손님많이오고 매출늘음 ?|0 +그래서 아침10시까찌 번호표 받을라고 기다리는거임|0 +필리핀에 코로나 확산 조짐이 있다고 하던데...|0 +부러우니까|0 +이게 다 빨갱이 때문이라 할겁니다|1 +진실로 진실로 속히 이루어질찌어다. 아면.|0 +와 한가인은 진짜....여신..|0 + 돈가스 샤부샤부 라멘 카레 어디서 유레한 음식 이노? ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +원래 결혼해서 알콩달콩 잘 사는애들은 이런데 글 안올린다 올릴 이유가 없지결혼생활 씹창난 놈들만 결혼하지말라고 광광 우는거야|1 + 병신 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +ㅋㅋㅋㅋ씨발|1 +머래 홍어새끼야 예전엔 돈이없어서 가방끈 짧았으니까 제조업으로라도 먹고살아야되서 인구많은 경상도에 공장 많이 지어서 용접공이 많은거지 경상도 애들이 머가리가 딸리는게 아니다 홍어색기야|1 +IT사업가? ㅋㅋ왜 고소당할까 무서워? ㅋㅋㅋ|0 +남자들 성욕주체못하는 병신만들고 뭐만해도 처벌받게하려는거지|1 +소갈비이이이이이잇!|0 +0랩 새끼들은 닥치자|1 +ㅇㅂㅋㅋㅋㅋ|0 +얼마나 관장을 잘하면 주짓수하면서 관장하노 ㄷㄷ|0 +유니폼색도 딱이야|0 +이미 알고 있어서 ㅁㅈㅎ주려고 했는데1렙이라 ㅁㅈㅎ 못줘서 속으로 애미 욕함|1 +선생님이 틀린말 하신거 아니었네 |0 +추천한여자도 짤라야된다|1 +여대생 냉찌꺼기 빨아먹고싶다 |1 +근데 저래 가버리면 그것도 안될것같고|0 +보통 노가더들이 나처럼 되지 말고 공부 하라는 충고질을 하는데머리 나쁘면 공부 아무리 해봤자 시간과 돈만 버림공부야 말로 씹 재능충들이 하는 일인걸 모르고 저렇게 말하는 경우가 대다수고더러는 알면서도 자기 머리 나쁘다고 말하긴 싫으니 저렇게 말하는 거고결국은 사람은 자기 팔자대로 사는 거임.노가더들은 공부하라는 충고 대신 일하는 노하우나몰래 농땡이 치는 팁을 전수하는게 오히려 듣는 사람 에겐 공부할 동기부여가 더 됨|1 +비위좋노 게이야 취존한다이기|1 +그래서 너 기분나빠?|0 +그거 필리핀에서도 봤고 발리에서도봤음. 발리에선 배에서 내려서 수영으로 해변까지가는데 그 절벽보다 멀리서 내림. 그래서 바닥안보이는곳에서부터 수영시작해서 갑자기얕아지는데까지 맨몸으로 수영해서가는데 스릴 ㅆㅅㅌㅊ|0 +박근혜 개씨발년|1 +지원하고 싶네요.|0 +전라도 +20~40 여자+ 골수좌익들 그럼 저 지지율이 맞아 아니다 아니다 정신승리하다 총선때 쇼크 먹지 말고 |0 +먹방은 역시 쯔양|0 +조만간 주진요 만들어지는거아니냐 |0 +병신아 걍 인터넷에 찾아보면 되지|1 +하도 욕처먹어서 오래 살겠네~ 그렇게 욕처먹으면서 댓글다는 이유가 뭔가? 이게 직업이니? 650원? 너도 참 애잔하다ㅠㅠ 나이 꽤나 먹었을텐데...|1 +닉갑 오지고 병신색기|1 +오 전라민주당 굿아이디어.|0 +저거 대본이잖아 글쓴이 선동 ㅁㅈㅎ|0 +오작교입니다|0 +한번써본거라니까ㅋㅋ 한국인쓰지ㅋㅋ|0 +철들었을때부터 같이 살았을거아녀|0 +일해서 사먹어|0 +ㅋㅋㅋㅋㅋ그래 차라리 뛰어가는거보다 자존심버리고 걸어가는게 서로 낫다|0 +정신지체장애 국가공인 1등급|0 +정부도 대구시에 돈주지 마라|0 +성화봉송, 일본 도착한 날 강풍으로 꺼짐 ㅋㅋㅋ|0 + 머가리 터진새끼는 ㅁㅈㅎ|1 +막상 영남공대 금오공대도 소수들만 취업 잘하지 거의 70~80 좆소 따리인데 딸 존나 치면서 서성한 인문상경보다 취업 잘된다는 꼴.....|1 +마. 끝판왕 wbc빼고 중간보스들만 들이미노?|0 +저딴거 하고 다니면 극혐인데 그냥 집에서 지들끼리 똥꼬섹스만 하고 평상시에 밖에서 남한테 피해 안주면 노상관이란거임|1 +진짜..서울에..미사일한방만....|0 +복지제도가 사회주의사상인건 아니?|0 +오체분시, 능지처참|0 +솔직히 중국인 입국 거부하는거 찬성한 애들은|0 +상식적으로 대리예약하는놈들이 그런다고 못하겠냐 계정 받아서 그걸로 매크로 돌려서 예약해주겠지 그러다 개인정보유출 2차피해 헬피엔딩까지도 이어지기 씹가능이고 병신아;|1 +그냥 북한애들보면평생 허벌창으로 사는게 이 나라의 역사엿단말인가 라는?? 비통함과 비참함밖에들지않는다 하..|0 +한달에 한두번 집에 갈 수 있는 그런 미개한 근로 조건이라면 누가 하겠음?|0 +민주주의의 폐해지 ㅋ 도편추방제같은거임 ㅋㅋ대다수는 누가 뭔 일을 했는지 관심없지. 그저 누군가 물어뜯을 거리만 생기면 생각없이 물어 뜯어댈뿐 ㅋ거기에 민의를 대입하고 정의를 녹여넣으면서 스스로 자기위안삼으면 끝|1 +버블도 터져서 돈다발로 보빨 가능한 남자도 존나 줄고 니트가 되어버리니|1 +수조원 이지랄났네 미친새끼 ㅋㅋㅋㅋ 이건희 재산이 19조인데 수조원?? 정신나갔냐 ㅋㅋㅋ|1 +공부에 흥미 없으면 차라리 용접 배워서 돈 많이 벌라는 선생님의 따뜻한 배려로 들리는데 정상이냐...저렇게 말해주는 선생님이 진짜 선생님이지..|0 +예전 텔레비전에서 옛드라마 리뷰해주는 프로그램이 있었는데조형기가 예전에찍은 드라마가 하나있었는데 그냥 동네 놈팽이였는데 저수지 관리라는 완장을 채워줌그뒤로 권력 아닌 권력을 휘두룬다는 드라마였는데 나중에 함 찾아봐라사람이 완장차면 어케 변하는지 딱 보여준다|0 +상주 라인업보소 ㅋㅋㅋㅋ|0 +여자가 고추만지고 튄다레드라이트|1 +예전에 뉴스에나왔잖아 횡단보도에서 담배피던 중국인반달한테 쳐맞아서 죽음 |1 +당사자앞에서만 숨기는거지 배운 사람들 선민의식 없는 사람없다 ㅋㅋㅋ내가 지잡-서성한 공대-약대오면서 여실히 느끼고 있음인성에 따라 대놓고 말하느냐 안하느냐의 차이지 다 있어 ㅋㅋ|0 +더티 핑크당|0 +나도 개좆흥 옹호하는거아님 개좆밥인데 뭘 옹호해 , 다만 버기는 수비롤 감안해도 윙어포지션과 초창기 윙포였던거 생각하면 스탯 처참하다못해 개박살난거 맞다 ㅇㅇ |1 +방구석에서 귀찮으니 시켜먹어대는 새끼 늘어나니까 저런 병신같은데도 늘어나는듯ㅋㅋㅋㅋㅋ|1 +내 친구도 와이프 눈,코해서 괜찮게 생겼는데 딸래미 낳고보니 개빻았더라친자검사 해봐야 하나 싶은 수준임|1 +내 주변 보면 다 버는 것에 맞춰 살게 되어 있더라. 근데 기집년이 정신 못차리고 남자가 버는 것에 비해서 바라는게 많으면 그야말로 헬게이트 펼쳐지는거고 남자가 개고생하는거고, 여자가 이해하면서 그에 맞춰 소확행하며 살 수 있으면 월 250 벌어도 괜찮은거지 |1 +겟앰에서너봣음|0 +이갈로>까꿍이|0 +아.. 또 눈물이..|0 + 일베가 나라의중심을 지키고있어요!!!!!!|0 +나라가 적극적으로 관리해도 국민이 돈이 있어야 수요가 있지 소득수준은 10년전이랑 크게달라진것도 없잖아|0 +미국보다 살기 좋다고 판단하게 된 계기들 들어보면다 병신 같음한국 부자들이 왜 미국 가서 사는지를 생각 못하나봄글구 솔직한 이유는 미국보다 살기 좋아서가 아니라미국에선 경쟁에 밀려서임미국에서 뜬 애들은 절대 한국 들어와서 살 일이 없음가수 등 연예인들 봐도 싹 다 미국에서 안먹하는 애들이 들어오는거유승준이 왜 미친듯이 한국에 들어오려고 하는데미국에선 듣보잡이거든|1 +그냥 만주정도 얻어서 철도 까는걸로 만족했음|0 +병신새끼들|1 +수학만잘하고 영어 국어 언어영역쪽이약해서 그럼 수학은 만점받음|0 +정치 얘기로 아가리 먼저 터는 새끼 10에 9는 좌파성향 ㄹㅇㅍㅌ|1 +이제 신안은 범죄률 0%의 청정구역 되겠노ㅋ|0 +나경원 "'우리' 일본" 발언 논란|0 +몸도 병신되고 ㅋㅋ|1 +86 거거익선|0 +재계 1위는 소뱅아니면 유니클로 아니녀|0 +바키 다봤는데 그걸 왜 못봤지|0 +그놈들한테|0 +잘하는걸 잘한다고 해야지|0 +경찰버려도 되네 지능 무력 혼자다가능|1 +2. 문씨가 제일 잘하는것이고|0 +힘있어도 저지랄하는건 똑같을듯?|0 +- 2019.04.21 일 오후 6시|0 +그냥 남혐걸린 병신집합소인데 뭘 그리 심오하게 생각하노|1 +닥쳐 틀딱련아|1 +독일외 유럽에서는 이 사건보고 전형적인 독일인 짓이라며 욕했죠.|0 +아니 저거 못읽냐고 난 대답 했는디 번역 까지 해줘야됨 초졸새꺄? |1 +저런 부류 보지년들 트럭에 두 번 깔고 맨홀에 틀어박고 싶노|1 +륜 임?|0 +저 여자 대기줄컷 당함.|0 +난 별로요|0 +수지 화장 벗긴모습이랑 노무 닮았는데|0 +지려라.|1 +김치 좌빨들 닮앗네|1 +존나 자꾸 개소리 쳐 할래? 내글에 중대 욕했노? 질 떨어지는 학교라 깠노? 왜 ㅈㄹ이야 자꾸? 너 무슨 피해의식있냐?ㅋㅋ|1 +운알못들아 저건 싼타페도 개씹새끼인거 맞아 ㅋㅋㅋ저딴 경우는 나만 살면 되지라는 희대의 개새끼임 |1 +사원 4년차인데 동기중에 스쿼트 230치는 미친새끼하나 있는데지내 부서 여자과장이 술먹으면 자꾸 엉덩이랑 하벅지 만진다고 한탄하던데ㅋ 정도경영 찌르면 지가 쪽팔리고 그냥 넘어가자니 쭈글탱이가 만지는거 좆같고 스트레스받는다더라|1 +지랄도 풍년이구만;|1 +용접보지라고 부르자|1 +앞에 초록색 문자뜬거 말하는거냐??? 눌러서 신고할까 하다가 기분나빠서 안눌렀는데 ㅈ될뻔했노 ㄷㄷㄷ|1 +문재인 애미씹창년새끼 ㅇㅇ|1 +7등급이 어떻게 용접을해ㅋㅋㅋ배달이나 막일해야지 |0 +버리고 갔네 ㅅㅂ... 간혹 산에 노견들 버리고 감 |1 +.... 자기한테 수학배우는 애들도 용접이 시급할것 같은데|0 +어디 끌려가나|0 +저희 주차장에는 미제밴을 1년 넘게 세워놓고 안빼는 차가 있었는데, 결국 견인해 갔습니다.|0 +10만원이면 ㅆㅅㅌㅊ 돈까스 쳐먹고 말지 |0 +금리 10프로 옛말이다 .. 거기서 3년전까지 일했는데 그때도 7퍼 되나마나 했는데|0 +ㅇㄱㄹㅇ임.능력있는 부모는군전역하면 상타치 직장,거주지 전세금차한대 뽑아주더라. 이거 3단 선물 못받았으면 부모한테 돈갖다바치지마라. 일단 인맥 자체가 완전 다른 물에서 시작하고 사람들 대우가 다름.|0 +국가 곳곳에서 분리주의자들, 공산주의자들이 설치던 씹개판국가였던 시기임. 소수민족들 폭동도 수시로 일어나던 시기에다가 냉전이랑 맞물려 소련이 이란에 분탕치기도 햇엇고 오히려 국가 내부의 안정이란 측면에서는 호메이니 시기가 훨씬 나았음그리고 팔레비도 사실 알고보면 잘못된 정책으로 나라 말아먹은 지도자임.문재인이 성향만 친미로 바꾸면 딱 팔레비임저 사진들은 그냥 이란이 문화적으로 좀 개방되었던 시기였음을 증명하는거일 뿐이지 이란이 잘살았다는 방증이 아니지저 사진보고 팔레비왕조 이란이 잘살았다고 생각하는 새끼들은 북한 평양사진 보고 북한도 잘사네 ㅇㅇ 이렇게 생각하는 병신이랑 진배없음|1 +전 뒤에서 존나 뛰어가 날라차기 하고 도망갈 예정임|1 +조1센 자영업자는 솔직히 망해도됨 술집 들어가기 전에 메뉴사진 보니깐 불삼겹볶음이란 메뉴있길래 사진보니깐 엄청 푸짐하던데 시키니깐 양파쪼가리, 대패 몇개 이렇게해서 7천원 받아처먹더라 |1 +예전에 일베에서 본거 세계 유흥글 싸던 게이 어디갔노|1 +뚜벅충 존나게많네 병신새끼들 싼타페가 씹새끼지 그럼 저기서 안전거리 미확보하고있다가 칼치기로 빠져나가는게 정상이냐 뚜벅충 씹새끼들아뭐 부비트랩이냐 씨발 일부러 엿먹였다고해도 할말 없겠다|1 +착해졌노|0 + 눈물이 계속 나와서 그렇지|0 +너 딱 나랑살면알맞겠다|0 +문과에서 허가 안해주면 못만들어~ 이과는 문과 아래야 알겠어?? 왜 문과 욕해?? |1 +추천하고갑니다.|0 +첨엔 좋았는데 길어지니까 오히려 스트레스노|0 +ㅋㅋㅋ용접협회?ㅋㅋㅋㅋㅋㅋ|0 +저런거 혹하는 새끼들이 용하다고 사주같은거 잘 믿지.|1 +안하는게 정론인데 홍어세리네이거 ㄴㄷㅎ|1 +역대급 추한 댓글 |1 +구글 업로드느리자나|0 +그게 바로 비싼 천연비타민제품들이 내세우는 강점인데 비타민이 천연 합성 나누는것도 웃긴거임 뭐 어떤비타민은 화학반응일으켜서 창조해내냐? ㅋㅋ 천연비타민도 그 제조과정에서 추출 압축 응축시키는 과정이 다 화학처린데 걍 너 상술에 놀아나는거임 같이 먹는 시너지일으키는 성분들을 잘챙겨먹는게 더 중요 가녕 내가먹는 L시스테인은 알파리포산이랑 함께 섭취시 흡수율이 20퍼정도에서 40퍼로 높아지기때매 같이먹는다근데 이방송이 일단 영양제 자체 무용론을 얘기하고있으니 그거에대해 초점을 맞춰야할듯|0 +박사가 여러명이다 진짜 박사는|0 +ㅋㅋㅋㅋ 존나 맞는말. 나도 지잡에서 편입했는데 레알 이름 좀 있는 대학이라 그런지 공기업 대기업 붙었다는 말이 여기저기 난무한다. 취업난이 맞나 싶을 정도로 되게 신기하더라. 지방은 지거국만 가도 지방거점 대기업 공기업에서 서류탈락하는 일은 없더라.근데 결국 자기 하기 나름이다.|1 +세계사에 대한민국 민주주의 몰락 관련으로 다뤄질텐데 신기허구만|0 +사랑해서 사고로 정화원만들고?|0 +무조건 정부 깔려고 작정한 넘이라..|0 +허경영 총재 함부로 욕하다간|0 +관상은 과학인라고 하는데,|0 +같이 잘먹고 잘살자? 그럴 순 없어 그건 공산주의지.우문현답같은 소리하구 자빠졌네ㅁㅈㅎ|1 + 너는 누구의 후손이냐...|0 +당근이쥐!|0 +_이건 왜 써대는거임?|0 +조중동의 아류들|0 +노무 슬프노 ㅠㅠ|0 +결혼 9년차보험료, 공과금 같은거 공동 생활비 통장에 넣는거 빼고는각자 수입 각자 관리함|0 +애미뒤졌놐ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +수정이 지금 책상밑에서 내 ㅈㅈ 빨고 있는 데??|1 +코로나 19 때문에 취소가 된다면 경제적으로 진짜 거지될 듯..|0 +문재다문재 승징빙세끼들..|1 + 그러면 안가는 날듯... |0 +비하면이라는 말 뜻을 모르노?|0 + 니가 근거 가져오면 니가 이기는 게임 아니겠노 이 씨발년아 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +노건즈 라이프 ㅇㅂ|0 +나도해주라 병신천지 씨벌것들아|1 +헬스장도 떳다방임 오픈빨일때 팔거나 못팔면 잠적 이런새끼들 좆나게 많음 |1 +죄송합니다...ㅜㅡ|0 +강남구, 송파구만 살아봐서 잘알못|0 +고속버스들어가는게 쉬운것이 아니라 알고 있기에 키보드로 손털어봄|0 +김성회가 어찌 10번이냐 안원구님하고 바뀐거 아냐???|0 + 너 이중에 몇가지 해당되냐 솔직히? ㅋㅋ|0 +울동네는 맘충들없음.|1 +공부해서 설계사되면 되는거아니냐|0 +ㅇㅂ에 이런글 올라오면 딱 3일후 팔면됨.꺼억~~~ 잘먹겠다.|0 +연애할땐 좋았지 병신아???? 하하하하하하....휴 다행이다 싱글이라|1 +일게이들은 공감 못하겠지만 이게 사실이라고함. 일베금수저도 그런말하더라 misery is doubled 래 그걸 잊을라고 마약하는거지|1 +펭수가머가귀엽노?? 얼굴이노무하얗고 눈은무섭고 입도이상한데..|1 +다리기술이 얼마나 위험한지 잘 모르는거 같아서 잘못맞으면 사망이야.|0 +변희재 김세의 강용석은 서울대출신 엘리트 애국보수들임|0 +남의 걸 왜 지좆대로 씨발 어이가 없네저렇게 강제로 무식하게 하면 약만 쳐올리는거고.아픈건 본인이 극복해야지 주변에서 염병떤다고 나아지는게 아닌데|1 +토토로 돈벌기 힘듬 강원랜드생각하면됌|0 + 그거 지적하니 바로 글삭 ㅋㅋ|0 + 다 현장직들인데..|0 +저런 배달음식점에서 곱창을 직접 씻기는 하나?|0 +저건 둔근운동이야|0 +가입일보셈 ㅈㅈㅂ뜻을 모르거나 정게만하다 부캐판 틀딱이거나 ㅋㅋㅋㅋㅋ|1 +진박이형 어쩌다가 저렇게 바보가 된거노....|0 +피스팅으로 혼내주자|0 +크하하하하하하 헙(웃다가 틀니빠짐)|0 +마누라 큰놈 낳고 초유가 안나와 빨아라 해서 빨아 묵음 좃같더라|1 +종북짓 했다고 지금 처음 말했는뎨? 빨갱아|1 +인도에 확진자 숫자가 적은게 미스테리|0 +충분히 가능성 있지. 미국이 대만이랑 동맹관계였고 대만섬에 미 육해공군 주둔시키던 동시기에 제3국 폴란드 인민공화국에서 미국대사랑 중공대사끼리 대사급 비밀회담하면서 중공이랑 국교정상화 의논하고 대만단교 계획했었잖아. 이걸 대만단교 선언했던 1979년으로부터 14년 전인 1965년부터 미국이 중공이랑 비밀회담 했었지. Robert Sutter이 저술한 책 US-China Relations에 자세히 나오니까 미중외교사에 관심있는 사람은 읽어봐라.|0 +취업 조차 못할텐데.|0 +4년제가 대단한거냐? 4년제 아니라 40년제 대학을 나와도 경쟁력이 없으면 최저시급 받는거야.|0 +콧구녕이 작아야 명기|0 +어이구 병신들ㅋ|1 +아베 나베 남매 따까리인 듯...|1 +심상정 욕하는 사람은 대부분이 민주당 지지자들인데...|0 +오..자비롭군요 mr.봉산|0 + 그리고 중견이하 중소 좆소는 솔직히 복불복이야.|0 + "대기업에서 걍 생각없이 탱자탱자 공무원처럼 일해도 무난하게 부장 단다고?" |0 +그건 옛날 얘기고 요즘은 여자들도 suv좋아함 |0 +근데 개돼지 중 18% 이상이 음주운전 경험이 있어서 ㅇㅇ|1 +사진땜시 기분 잣같아져서 ㅁㅈㅎ|1 +쟤가 뭐가 이쁘다는거냐 그냥 아줌마같은데|1 +나붸붸 화이팅~~~~|0 + 너나 영어 모르면서 나대지마라|1 +예전에 원룸이사해서 트럭불렀더니 틀딱아재가왔음.아재하는말이 예전에 자기가 소.돼지 이런거 도살장까지운반하는일했다고함.그런데 지인이 원룸 이삿짐하면 돈이 더된다고해서 이거시작했는데 하루죙일일해야되서힘들다고함 ㅋㅋ 도살장은 새벽에 한탕뒤면 끝이라고함자기는 소끌고갈때는 불쌍해서 바닥에 흙이랑 짚을깔아줬다고함.그러면 소가 탈때도 맨 차바닥과 반응이완전 다르다고함. |1 +수학 1타 강사들 말고는 그저 그럼....연고대 출신이라면서 알고보면 분캠인 경우도 있고|0 +남자랑반대네남자 168 9는 들어본적없고 줴다 170 1 2 이러던데 ㅋㅋ|0 +같은공대생으로 힘내라고 ㅇㅂ|0 +부모 욕하는 사람은 자식 입장에선 선처하시면 안된다고 봅니다.|0 +진짜다 게이야|1 +코로나19 집단발병지 인근엔 미군부대가 있지. 우한에서는 작년 가을 세계군인체육대회를 했어.|1 +AV좆문가들ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 + 그래서 내가 아예 썅그지 동네도 개인택시 없으니 예시가 잘못됐다는 말인데 혼자 뭔 씹소리야|1 +각종 커뮤니티에 존나 떠도는 조크라 ㅁㅈㅎ|1 +ㅇㅇ가서 대가리 깨버릴거임|1 +윈디 마시지 위치까지 검색했다 접었다.|0 +왤케 더 예뻐진것같냐|0 +통진당 종북으로 몰아서 죽인건 박근혜와 새누리당이고..|0 +할일 없겟노|0 +시발 체다치즈 그거 비닐냄새 나서 비위 약한 애들은 못먹어|1 +사이트 출신분|0 +유니클로에서 계산 대기하는데 와이프 애들이릉 같이 온 아재 날 존나 무섭게 쳐다보더라 보통 2초 이내로 쳐다보고 피하는게 정상인데 이 아재는 3번 쳐다보더라 |1 +부산임|0 +뭔소리지 ? 그순교자들중에 한명이니까 팩트에서 틀린게 없는데 ??|0 +돈 뿌리는거|0 + 너의 의견이 궁금한거임 ㅋㅋ|0 +강간범을 강간하는 강간범이라니... 다크나이트인가...|1 +너 이 댓글한번더 쓰면 아이디 삭제빵한걸로 간주하고 댓글이랑 가입일 박제한다|0 + 다들 채식하자 ㅆㅂ 그러면서 고기를 먹는 나는 ㅠㅠ|1 +기다릴께요,,,ㅠㅠ;;|0 +귀차나|0 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 근데 함소원 중국인처럼 생김. |0 +Just way you are 빌리좆엘|1 +오락실 재떨이|0 +두번이나 기각 때린 짜장클라스|0 +글삭하지마라 스크랩했다|0 +효진언니를 노렸어야지...|0 +중고차딜러는 왜 끼냐?|0 +내자식들이 살아갈 대한민국입니다.|0 + 바퀴벌레마냥 불법으로 모여사는|0 +니애미씨발년|1 +그거하면 빨리 죽어|0 +@한잔술에A |0 +난 정의로운척 하는 인간은 극혐하는데 미친새끼는 좋아한다. 앞으로 조국수호 지지하려한다. |1 +올 가을쯤 되면|0 +공산당이라|0 +미안하다이기! 심심해서 장난치고 있는데 지켜보는게 괴로웠다면 사과할게. 잠탰다! ㅠㅠ |0 +자지네이터 실사판을 볼줄이야|1 +4년쯤 전인데 그 다음날 sk해지하고 lg로 갈아탐|0 +바지 넘 이뻐요|0 +와!!! 이런걸 원했어요|0 +안써봤으면 걍 찌그러져있어~ 어쩌다 한두번 맛배기나 봐놓고 커브드 왜 사냐 이딴 소리 하면서 정신슨리하지말고|1 +도쿄올림픽 취소되고 보자..|0 +어쩔수없자나 ㅋㅋ|0 +그게 아니지중대 수학과가 서울대 경영학과 보고 내가 문돌이였으면 서울대 경영갔다 이런 소리 못한다는 거지|0 +그 동네애들 먹는거냐? 어디서 만나서 머그심?|1 +전 드한 개새끼|0 +와 씹고수 ㅋㅋㅋ|1 +현금이 많으면 노인들 죽었을때 청소업체에서 꿀꺽하는돈도 많겠네 개꿀|1 +오늘 월요일아니야? 나 지금 너무 혼란스러워....|0 +개주작 반토막으로 유동성큰데가면돼지 질질짜기는|1 +내 사촌동생이 진짜 머리멍청하고 성실한 편인데 공무원 1년 꾸준하게 하니까 바로 붙음 |0 +코로나 사망과 해당 사진은 관련이 없다고 해도 저렇게 해서 나오면 솔직히 노린 거라고밖에 안 보이네요 ㄷㄷ|0 +ㅎㅎ|0 +일베하면서 정상인컨셉진짜 ㅋㅋㅋㅋㅋ 좆병신같네 ㅋㅋㅋ 아니지 차라리 컨셉이면 낫지 진심이면 걍 딴데가서놀아라|1 +좀 오바좀 하지말자~|1 +요번 겨울은 참 따뜻?했다우모량 빵빵한 패딩은 손도 안댔음앞으로도 이러면 좋을텐데|0 +저거 딱봐도찐따새끼가 일진형님 휴대폰번호로 장난치는거같은데ㅋㅋㅋㅋㅋㅋㅋㅋ|1 +화가 많군|0 +초대남 줄서봅니다|0 +원래 이륙할때 노말한 상황에서는 플랩 안쓴다..|0 +니말대로 2060년에 휴거가 있을수도 있고 |0 +오렌지새기 딱조타|0 +요즘은 간호조무사도 저런책 보냐?|0 +집앞에 두고 가면 내가 혹여 바빠서 좀 늦게 가지러 나가도 집 앞에서 물건이 신선도 유지하면서 안전하게 잘 기다리고도 있지요.. 종이봉투에 담긴 쓱 배송 내용물 조차 누구 하나도 무엇 하나도 집어가지 않고 그냥 그대로 두던데요 ㅋㅋㅋ|0 +아니 원래 자기 위주로 돌아가는거지 그거 가지고 지랄하노 이런거 보면 이나라는 뭐로 가든 전체주의로 갈 운명임|1 +JUY-119 |0 +나랑 학력인증 할까?|0 +헐|0 +블루컬러 직업에 대한 인식이 아직도 이러니 ㅉㅉ|1 + 하지만 그것이 닝겐이 말하는 신일지는 확신할 수 없어|0 +어이가없어서 50명 재보니까 50등따리 더라고여 래시포드가^^ 시벌|1 +보나마나 전라도 |0 +저렇게 동거해보고 여자가 게으르다?칼같이 헤어져야함안그러면 니기좆됨|1 +게이야 내친구들이 순경이 많은데 순경의 미래는 으떻노 ㅅㅌㅊ노???|1 +부모님의 노력이지|0 +그렇구나 K1진짜 뭔 생각으로 그렇게 작게 만든지 모르겠더라|0 +ㅋㅋㅋㅋ|0 + 저능아가 일뽕인거 좀 실망이다 |1 +니미 애미 씹보지 븅신아 ㅋㅋㅋㅋㅋ|1 +이기|0 + 오늘 일해서 일당 벌었으면 가서 매독 걸린 니미 애미 보지 1000원 주고 실컷 쑤셔라 ㅋㅋㅋㅋ|1 +??????????|0 +대접도 받는데 얼굴만 예쁜 골빈 여자라는 편견도 있는거|0 +앗! 이것은 사탄의 컬렉숀...|0 +차라리 그냥 무조건 싫다고 해.|0 +힘없는 개인을 저렇게까지 까면서 돈벌고 싶나 시바|0 +맛녀석은 동시간에도 채널 여러개에서 재방해주더만 ㅋㅋㅋㅋㅋ|0 +올림픽 메달따면 군대 안가고 산업동탑 훈장 주고|0 + 이게 애들 가르친다는 사람이 넌 공부 못하니까 다른거 알아봐 라는 마인드가 깔려있으니..|0 +이뻐서 무죄야|0 +근데 그 부위를 좋아하던데 음... 저런거 보지말고 실습해서 익히자.|0 +미국 쟈들 전략이었던게 아닐까|0 +그럼 살 어떻게 빼노|0 +친구가 명의는 어차피 이혼할땐 아~~무상관없다던데ㅋㅋ 자기는 그냥 명의주고 평생 생색낸다더라 |0 +의료민영화는 영원히 하면 안되는 것.|0 +베트남 따위가 차세대 성장동력도 없는데중진국까진 기적적으로 도달한다 해도그 이후는 뭘로 견인해나갈건데주력산업이 농사밖에 없는놈들이|0 +저거 퓨마임 좆된듯 ㅋㅋㅋ|1 +그냥 처벌 받아라. 정신개조가 될라나 모르겠다만....그래도 처벌은 받고 그 다음 생각해보자.|0 +공무원 ㅋㅋㅋ웃고간다|0 +춤추는 사람들이노ㅎㅎ|0 +간만에 이단어가 떠오르는군 "이뭐병"|1 +메종일각이?저거 루미코 여사님 작품아님?|0 + 중국 유교|0 +보상심리 때문에 비리 저지르는 일이 많지 않겠노? 사법고시 합격한 사람들 보상심리 엄청나더라|0 +하나가 되자!|0 +그게 귀천이 있는 거지 귀천이란 게 물건도 아니고 각자 의식 속에 있는데|0 +7차선클라스 ㅁㅊ|0 + 실제로는 한달에 세네번 같이 본다|0 +꼭 그런 거지새끼들이 생명이 돈보다 중요하냐 이지랄하는데|1 +이나라 친일친미사대주의자들때문에 머리노라면 다 미국 사람인줄 알던 시절이 있었는데.. 외국 사람이 미국사람 일본은 미워도 배워야할 나라 .. 짜증난다|0 +호다다다다다닥|0 +지금 미국활동중이라 불가능할텐데...|0 +맞음|0 +남녀 성비 50:50 맞추지 않아도 무방한가요? 몰표 줘야되는데 여성후보자가 어떻게 들어오게될지 궁금하네요.|0 +못기어들어오게하고 필리핀서 처맞아|1 +용접공한테가서 "공부못했으니까 지금 용접하고있는거에요이렇게 말하는거랑 뭐가다르냐 쟨 끝났어 이제|0 +외제차 탄 애들이라 생각이다른 바퀴임|0 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +경희대 법학이냐? 문재앙 후배? 분탕새끼|1 +물 없이 담그는 오이지냐|0 +이야 훌륭하신분 ㄷㄷㄷ 저도 친구에겐 돈 빌려주지 않습니다 그냥 주죠 돌려받으면 좋고 아니어도 그냥 그런갑다 하는거죠 ㄷㄷ|0 +사이버포뮬러 알정도면 나이좀 있는데 저지랄한다고?|1 +5등급 6등급인 빡대가리들이 그러지|0 +원래 양남들은 잘 안갈아입는다 ㅋㅋ|1 +성형후 사진 원래 포샵 존나하는거 아님?실제로 보면 오른쪽같이 안생겼을듯|1 +원숭이 네 망하는 소리가 들리네 ㅎㅎㅎ.|0 +쏘렌토 대비 가벼운 무게로 연비 15.8는 뽑을 수 있을테고..|0 +어차피 엔지니어도 테크니션 없으면 일 못함.|0 +눈 수술하고 일상생활 언제쯤가능|0 +응 지랄하지마 내얼굴에 침 뱉을래 좆짱깨 새끼들은 전원 자살해라 ㅋㅋㅋㅋ좆미개한 유교문화 좆미개 한자문화권 ㅋㅋㅋ 애미디진 공자를 쳐빨아댐얼른 미국&EU연합에 핵폭탄 쳐맞고 멸망해라걔들한테 항복해도 니넨 인도&러시아 동맹의 밥그릇임|1 +조립컴이 뭐냐? 나같은 컴맹은 못하는거 아님?|0 +아이템매니아에서 계정샀냐|0 +허위사실 ㅁㅈㅎ 2점짜리 문제 다맞아도 6점밖에안됨|0 +사람이 이렇게 추하게 변하기도 하는군요. 정말 곱게 늙는것도 복이네..|0 +하지만 그 모든 것을 망라하더라도 선수들을 위험에 빠뜨리면서까지 개최한다는 것은|0 +ㅋㅋㅋㄱ진중권 우파패널들 상대로는 털리던데 무식한 좌파상대론 여포네 ㅋㅋㅋㅋㅋㅋㅋㅋ존나 꿀잼|1 +좋은 자료 감사해요!|0 +맞음.. 내 주변 과고애들 중상위권 상대로도 강의 다 짤렸음 ㅋㅋ |0 +불심으로 대동단결 아직도 기억에 남는다|0 +2010년대 초부터 느낀건데 일베게이들 돈많은년들, 영어 잘하는 년들 존나 싫어함. 솔직히 연애 많이 하고 섹스도 많이 하면서 뭐라도 이룬 새끼들이 아무것도 못이룬 방구석 인생들보다 못날게 뭐있냐 ㅋㅋㅋ 현실은 워홀충 영어강사>>지잡방구석 편피노 일게이 이게 팩트 아님?|0 +제발 가서 돌아오지마라|1 +진짜|0 +아가리 똥내로 객사할 듯|1 +협의에 포함되어있지않냐 오천만원 송금 .. 그리고 핵심은 아직까지 사용하였던 핸드폰등은 제출하지도 않았고 압수하지도 못하였다그리고 부산 부시장 유재수 감찰 무마건도 조국전 장관이 책임자의 자리에서 있었고 그리고 지시한것의 그이상을 사법당국이 조사중이었는데 6시간 기다렷고들한들 6시간이전에 만난날 첫미팅때 의견이나 견해를 구한것이 아니고 통보만 하였는지 조국전 장관이 않혀놓은 두인물만 제외하고 모두 일사분리로 좌천 시키거나 더이상 조사를 하지못하였던것 의문이지않냐?선거사범을 초반에 인지하고 제압하여 나쁜정치적 관행이나 습관을 한국이라는 사회에서는 뿌리뽑으려고 하였는데 그것도 못하게 만들어 버렷어 유권자에게는 아주큰 손실 , 매우큰손실이지|0 + 이제 넌 좃된거임 |1 +아가리 그만 털어|1 +'소련이 자기들 붉은군대 대신 여자들을 앞세워 침공했다면 서방세계는 진작 무너졌을것이다'|0 + 그러니 공정하지 못했던 조국네 가족에게 화가 나는거고|0 +마누라 하나 못 잡고 잡혀서 사는 호구 병신 보빨러들이 얼마나 많길래 저러냐?|1 +니가 이야기하는 것은 건전한 토론을 하는 반대의견 제시자고.|0 +유튜브 자유의창 길거리미터보면 답나옴ㅋ|1 +진짜임?|0 +ㅎㅎㅎㅎ|0 +우욱 씹|1 +어디오유에서오셧어요?|0 +아니 걱정해주는데, 어떻게 발끈이냐 ㅋㅋㅋㅋ |0 +ㅁㅊ개독들|1 +슨타크 되게 젊은 애인 줄 알았는데 나이 많은 아재더만 다시 봐도 정말 안타깝다 ... 재활은 잘 되었는지 초긍정 마인드 ㅆㅅㅌㅊ|0 +그래서 문재앙 정부에서 담배 규제 하려하는데https://www.ilbe.com/view/11174260554박근혜 담배 잘했다고 하는 일게이들이 이건 또 욕하더라|1 +저래도 주급 따박따박 나오지않나?|0 +돌아가십쇼 형님...|0 +같은 급여 조건이라고 써 놓은 거 안 보이냐?|0 +머지??? 일성 정일 정은 부자 머라할게 아닌거 같은데??|0 + 내가 그런 말 했다는 근거 가져와 ㅋㅋㅋㅋㅋㅋ|0 +어제 길 잘못들어서 유턴할려고 잠시 멈췄는데, 고양이 두마리가 달려오더라... 안보이길래, 멈춰서 내리니까 차 바퀴랑 보닛 밑에 앉아있더라... 시골이었지만 길가 바로 옆의 가정집에서 주는 먹이 받아먹는 길냥이더라...|0 + 시발 난 이거 안쓰고 떡상할때까지 가지고 있을꺼다 .|1 +부들부들|1 +내년에 한국에 서브프라임모기지론 사태가 오나 안오나부터 한번 보자 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 시발 LTV DTI를 이렇게 강화하는데 ㅋㅋㅋㅋㅋㅋ 서브프라임이 뭔지는 아냐 병신아지금은 신용이 좃빠져도 주담대가 안나와 이 병신새끼야|1 +영상 보고왔는데....진짜 존경스럽네요...|0 +우리가 남이가~|0 +이 추운날 한강을 왜갔냐. 점심인데 따뜻한 국밥 한그릇에 소주로 풀고 집구석 들어와라. 인생은 하한가도 있지만 상한가 구간도 있다.|0 +사실 가불기적인게.. 또 가만히 있음 호구인건 맞음. 지금 중국한테 하는거처럼 말이지. 외교가 또 일관성 없는거는 문제인데, 일뽕들이 일본을 특별하게 여기는 마음만큼 일본은 안해주는건 일뽕들은 모름|1 +지휘계통 다 필요없고, 대대장님부터 전 간부 지통실에서 YTN만 보고 계시더라;;;|0 +저 벤치 좀 빨ㄹ 아니 쓸려고 |0 +그럼 비싸게줘야지 똑같이주냐?|0 +왜달라구했냐? 이씨바라|1 +그런 할아버지를? 저런 미청년이 인기없을리가|1 +누군가에는 첫사랑이엿을수도 ㅋㅋㅋ|0 +김구라 와이프랑 슈 봐라 여자라고 생활력 강하고 야물딱지냐?그냥 케바케이고 뉴스 기사 남여 비율 보면 연예인, 펭수, 동물, 드라마, 쳐먹는 거에 정신 못차리는 개보지년들과 달리 정치 경제 시사 상식 있고 사회 생활 더 많이 하는 남자가 돈관리 하는게 맞음|1 +맞음 일단 어제 저녁부터 지금 현재까지 깨끗함 나 방독면 쓰고 다니는데 사람들 시선이 꽤 느껴짐 ㅋㅋㅋ|0 +짤 선택이 왤케 틀딱같냐|1 +니뽕 전국체전 7월 확정...|0 +아까 비슷하면서 더 웃긴걸 봐서 그래 ㅋㅋㅋㅋ|0 +걔네한테도 기본섹스는 기본이지. 본문의 짤처럼 저지랄하고다닐정도면 당연히 변태고 동성애자라서 억압받는다는건 변명이기북딱. 피스팅 sm 스캇은 지들끼리좋아서하는건데 변태는아니지. 인간은 그냥 성욕장애생물인듯|1 +버려야지 한번바람피운건은결혼해도 또 바람피운다|0 +ㅆㅂ ㅇ게이들 언제부터 표준어 구사하고 지랄했노짜면 짠대로 짜브면 짜븐대로 ㅆㅂ 새끼들 다들 알아들으면서 서울체 아는체하기는.,.ㅎㅎ행복해라 게이들아난 짭은건 빡시더라|1 +너덜너덜 ㅋㅋㅋ|0 +안함|0 +돈쉽게 벌려는아프리카는 ㅁㅈㅎ .|0 +아이 개겉은년|1 +세계경제를 꿰뚫고있노|0 +원하면이라..|0 +키즈나아이 ㅋㅋ|0 +저긴 대한민국맞나?|0 +홍어야 |1 +처음엔 안쓰러웠고 쉴드도 많이쳤는데 워마드 빠는거보고 돌아섰다 진짜 나라를 걱정하는 애국자들이 아니라 박근혜 팬클럽 그이상도 그이하도 아님|0 +뭔 씨발 망신만 주구장창 당하네 |1 +니말이 맞음|0 +어이 김씨 폰만지지말고 일해|0 +요즘아이들이 아니라|0 +리설주 ㅁㅈㅎ|0 +나 닮아서 안이뻐서 다행..|1 +신천지!! 광주가 최대규모 최다 피해자!!|0 +외제차면 ㅇㅈ이지 돈많을듯|0 +게이가 이해 해라!|1 +난아키호가슴 브라자입고있을때 제일좋음 니기미|1 +2 FA|0 + 국가혁명배당금당 말고는 대안이 없음|0 +1,226 동의 완료|0 +진짜 페미가 가능하긴 하냐?이상적인 공산주의 사회주의처럼 이상만 좋은 불가능한 사상아닝?|1 + 우리보다 작게는 몇백 많게는 몇억만년의 문명이 발달되었겠지|1 +애새끼들 처웃기다고 듣는거 개씹노이해|1 +쌈박하노? 그것보다..조선족 가게에 붙혀두고. 사진찍어서 중국사이트에 게재하는것이 더 좋을것 같다|0 +우리 위에 중국과 일본이 있다는게 함정|0 +달걀이 한판에 만원찍고 수입까지 했었음..|0 +그랬던 일은 한 번도 없다. 뭐? 20년전의 한국이 어쨌다고?|0 +정치를 좌우를 떠나서 도움받았으니 |0 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +최소 2개 소멸|0 +하시는데 눈물이 또로록|0 +ㄹㅇ토토충이나 폰팔이라고 했으면 이렇게 욕 안먹었다|1 +ㅁㅈㅎ 짤이라도 캡쳐해놓고 지랄해라 쉐복하지말고|1 +지랄발광들을 해봐라.|1 +존나 병신같네 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ |1 +게이 뭐하다가 가세연에 차단당했누 ㅋㅋㅋㅋ|0 +안가야긋네|0 +대구교대급도 안되는 학교다니면서 아가리는 왜턴거야?다른게이들 보는데지기싫어서?|1 +정의인 척 하는거지 전라도인들 속으론 쾌재를 부를 듯|0 +걍 짱깨랑 다를게 없음 ㅋㅋㅋㅋㅋ 오히려 더하면 더했지 절대 덜하지 않음 ㅋㅋㅋㅋㅋ 존나 천박하고 교만하고 집단주의적 사고에서 영원히 머물러 있는 병신민족 ㅋㅋ가끔 나타는 선지자들 덕에 그나마 유지되오던게 이제 그마저도 나올수가 없는 사회임 ㅋㅋ|1 +볶음김치에. 삶은두부 ㄱㅆㅆㅌㅌㅌㅌㅊ|0 +대구 싸잡아 욕해도 대구놈들은 아닥하고 있어라.|1 +저렇게 고의적으로 밀어붙여놓고 이제와서 합의라?|0 +착오로 송부? ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ갈데까지 갔구나 씹재앙 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|1 +일본 국내통계 미포함이란 말은 없네요.|0 +ㅇㄱㄹㅇ|0 +암내치즈누렁내 날꺼같음|1 +앞으로 이런게 문화적으로 자리 잡으면 무슨대화를 할수가 있을까|0 +것보다 내 자신이 아무리 우쭐거려도 나보다 더 센 사람은 있길 마련이고 모든 것에는 존재의 가치란게 있길 마련인데특정 직업군을 아무렇지 않게 비하한 사실 자체가 경악 스러웠다|0 +네 다 김씨|0 +육사 수석 후 자퇴 , 서울대 서경석이 |0 +ㅈ 자메이카|0 +대체 너 나한테 왜그러는거야 왜|0 +좋겠다.. 저런시장을 가진 대구는~ 선거하고 밥사먹으라고 돈준다잔아~|0 +니가 하는 언어 공인 시험 높은 등급을 네이버나 구글에 쳐보면 |0 +너도 나이들어서 그러지않기를 빈다컴퓨터 이후의 무언가가 나왔는데 적응못하고씨피유가 어쩌고 그러는 틀딱이 될수도있음|1 +직접 총장날인 찍어서 표창장 발행한 적 있는 직원 말은 왜 안들어? ㅋㅋ무슨 선택적 정보 취득이냐? ㅋㅋㅋ븅신|1 +여윽시 짱깨들...|1 +믿음 만으로 즉시 구원 얻는다는 복음을 |0 +인사하는거..|0 +여론조사 조작해서 징역2년 이라매? 그말은 여론조사 조작아니면 정당해산 아니란 말이잖아|0 +서현에도 요즘 구성남애들 지하철 타고 많이 놀러오는듯 시발 좀 꺼졌으면|1 +밀과 양은 볼때마다 추천~~~|0 +글쓴게이 몇살이냐?|1 +와~~ 아무리 생각해도 이거 씹소름이네|0 +모하비는 너무 할배분위기|0 +저기가 일베라치면 여긴?대깨문과 조선족 밭인가?솔직히 틀린말도 안했자나|1 +한국에서 땅값이 가장 비쌀 껄?|0 +오예 씨발 버닝썬 나도가능하냐 물고빨고맛보고 씨발 ㅋㅋㅋㅋㅋㅋㅋㅋ호족옆에붙어 응디믿고 살아가면되냐? |1 +ㅋㅋ 모든걸 거르는 일게이들|0 +돈쓰는게 일이네...|0 +진짜 알리드립은 12년도부터본거같은데 시발 8년째 저지랄하네|1 +일베충 중에 멍청한 놈 하나 선별해서 총알받이로 발탁, 앞세운 것 같은 느낌이 드네요|1 +고영욱도 절레절레|0 +똥푸산대ㅇㅂ|0 +그냥 안봐도 보임좌파=돈앞에선 같은편이고 뭐고 없음|1 +그럼 어딜감? 저 조건에 저거보다 훌륭한곳 구하기 힘들텐데|0 +생각하기 나름이겠지~|0 +근데 공장이 뭐 어때서 그러냐? 중소기업은 다 공장아님? 기계돌리는것도 기술이지. |0 +나도 이거로 맨손 딸 하는데 기분 ㅅㅌㅊ|1 +스스로 망치다니 놀랍다|0 +혐짤표시해라 ㅅㅂ 기분 더럽노|1 +비례매국당이라고 해라 |1 +강사가 돈 잘 벌어서 그런듯|0 +ㅆㅇㅈ|0 +배승희 끌어들이지 마라|0 +응 26살 틀딱|1 +왜이리 부들부들거리는거냐? ㅋ ㅋ ㅋ|1 +땜쟁이 ㅅㄲ들 ㅈㄴ게 ㅂㄷㅂㄷ데는거 ㅈㄴ 꿀잼이노ㅋㅋㅋㅋㅋ|1 + 초창기 주식병걸렸네 매매경력 1년미만예상해본다|0 +구치소가서 센조이나 하겠지?꼴좋다~~|0 +앱자체는 좋은데 배달대행이 생기면서 배달질은 떨어지고 구매자에겐 없던 배달료가 발생하고 서비스질이 팍 올라갔다가 팍 하고 내려옴|0 +개이득|0 +힘내세요 이탈리아~~~~~|0 +이것만 가지고는 쓸 곳이 거의 없지. 삼각함수를 공부하기 위한 선행조건이라고 생각하면 편함.|0 +뭐가 감사하냐 꺼져라.|1 +빨리 니 몸부위중 가장 풍성한곳을 문지르며 '나는풍성충' 이라고 세번 말해야 지금있는 모근이라도 지킬수 있음 -찐-|1 +여자를 겪어본 적이 없는 게이 ㅋㅋㅋ|1 +누가이쁨?|0 +방망이로 상사 뒷통수 갈기고 싶다는거|0 + 날짜를 못박으면 안되는데 울컥해서 좀 지나쳤다 |0 +실화냐... 숨 쉴수있노?|0 +1억|0 +루슬란 스타일이 개 간지. ㄹㅇ 더파이팅 키보같음ㅋㅋㅋㅋ유리턱이 존나 안쓰러웠지만..ㅜ 노빠꾸 파이팅 스타일ㅋㅋㅋㅋ|0 +외교부 영상가보면 벌레들 몰려와서 정신승리중 ㅋㅋㅋ|1 +아 이새끼 말귀를 못 알아쳐먹네.....수학이 허접이라는게 아니라 한번배운 단순입시수학으로 밥벌어먹는 강사가 병신이라고|1 +근데 진짜 웃기네요 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ|0 +쓰레기들.|1 +안할꺼면 글올리지마라 노잼|1 +링크는 이짓거리 하기전에 니가 선관위홈피들가서 찾아보면 되지 손이없냐|1 +싹다 2등급이 한대가냐? 요즘 입시 이상하놐ㅋㅋ|0 +다음댓글이 학력인증 링크가 아니면 걍 좆무식한 빡대가리가 자존심 살려달라고 애원하는 소리로 알고 읽지않은 뒤 씹겠습니다^^|1 +일개 도시수장이 한나라의 국방에 대해 왈가불가 할 자격이 있나? 주제넘게 나대네 역시 홍어형 협찬인생 씨발 수준답다|1 + 장애인앞에서 와 나라면 자살했다 라고말하는거나 마찬가지지|0 +헬조선에서 왜 문치매이 대통령 해먹겠어뜬구름 잡는 소리로 망각하게 만들고 허상을 심어주면 좋아하고냉혹한 현실을 이야기 하면 지랄발광함|1 +저때는 피임약먹어도 이미 늦은거냐|0 +나도 진짜 대학 방학때 친구랑 친구아버지 건설사 소장으로있는 현장에서 진짜 그 아버님 덕하나도안보고진짜 맨밑바닥부터 2달일한적잇는데 진짜. 착한사람도많고 선한사람도 많더라. 일도 진짜 졸라게힘듬어느직업이든 그렇겟지만. 진짜 개차반인 아재들도 꽤있었는데 그런사람들도 정은있더라.|0 +내가 맥북을 사는 이유는 뭘까?|0 +오 욕할라 했는데 맞는 말임.|0 +5번은사질과조금다름 신차는 새차라서 사고률을 낮게잡고 중고차느 차가 오래되서 오히려 고장이나서 사고날확률이 높아서 사고율을 높게잡는다 그래서 중고차가 오히려 보험비가 더비쌈 |0 +레이 블박이 상당히 궁금|0 + 그냥 아예 좃병신인데 뭐이렇게 혓바닥이 길어|1 +.... 틀딱이냐 ??갑자기 아이폰이 왜나와 ? 주진모 사건은 어떤 폰이던지 털리는건데, 폰이 도용당한게 아니라계정이랑 비번이 도용당했자나이해못함 ?|1 +저게 꼴리냐?|1 +역대급 철판!|0 +정형돈 진짜 말랐네..|0 +딱 우리엄마|0 +옛날에 댕댕이 2마리랑 냥이 1마리가 집 찾아가는 영화 머나먼 여정이라고 있었는데 그거 배경이 샌프란이어서 한 번 가고 싶었다|0 +관상지랄|1 +물론 최근 몇년간 좌파정부가 각종 사다리들을 걷어차고 있는건 맞지만, |0 +뭔 의심이야 어쨋든 그것도 여론인데|0 +ㅠㅠ|0 +인제 기레기 |1 +바세린 으로 어그로 끌던 sbs 에 놀아나는 개돼지들많더라 어휴당연히 타고나는게 중요하다 나는 비타민은 안먹지만 마그네슘 아연 등 효과 봤고음식 으로 섭취했을경우 전혀효과못봄 존나쳐먹었는데 앞으로 비타민b 하고 오메가3 도 먹을거다비타민 d , c 는 모르겠다 |1 +난 개신굔데 오늘은 카톨릭에 기도해본다 아멘|0 +그 둘은 이미 돈에 연연할 단계가 아니라 그랬을거 같음..|0 +n번방 관련자들 처벌하고 신상공개를 반대하는 남자들은 관계자들 말곤 거의 없음.|0 +신고하면 뭐하냐?대가리가 빨갱이새낀데...|1 +국방부가 아니지 정부가 해야지 ~|0 +그렇게 따지면 학교 선생들 다 잘라야지|0 +과대 광고 등으로 집행유예 6개월 먹었잖아|0 +90억 연봉 깎는다는데 5000만원은 줘야지 뭐1년에 180골 넣어야 감봉 메우는데 ㅋㅋㅋㅋㅋㅋ|0 +내가말하는건 극단적인예를 객관적인 상황인것처럼 말하지말라 이거다|0 +우리 삼촌짤은 내리줘 형|0 +보지선생이었으면 ㅂㄷ거리면서불러서 좆나 팻을듯|1 +마치고 집에 들어올때 문앞에서 기다려주니 힐링되니 괜찮네요.|0 +틀린말은 아닌데 아직 그단계까진 멀었다|0 +뭐야 어케받은거야나도 풀참인데 아직 모자란데?|0 +용서빌어라 ,하고싶의면 너들끼리 실컷해라 .|0 +이런게 개혁이죠~~|0 +신용등급확인좀 보자고|0 +비싼걸 싸게 살려는게 도둑놈 심보아닌가?|0 +ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ |0 +요즘 의대는 펠로까지 필수라 거진 10년은 버텨야 돈 번다. 30에 붙는거면 그냥 무당쪽이 낫다고 봄.|0 +궁예 역시 눈물연기 잘하네 |0 +스마트폰 어플 나오면서 아프리카TV 유명해지기 전에 인터넷으로만 방송 보던 시절은 진짜 개막장이였는데 그때가 진짜 재밌다 영상 있나 모르겠는데 함 찾아봐|0 +어차피 만족할만한 외모 갖고태어난거 아니면 그냥 자기 분수대로 사는게 편하지않을까? 성형할것도 아니면서 왜 이런 글만 주구장창 올라오는지모르겠다|0 +더 책임지ㅣ는 구조로 바꿔나가야 합니다.|0 +블박차 ㅂㅅ이네. 앞차 브레이크 밟으면 같이 밟아야지 그냥 가고있어 ㅋㅋㅋ|1 +개소리하면서 도망가지 말고 근거 가져와 씨발 새끼야 ㅋㅋㅋㅋㅋㅎㅋㅋㅋㅋㅋㅋㅋㅋ|1 +소속사가 갑질하지 않는이상은 잘벌지|0 +역시 이번 대통령은 촛불의 힘, 촛불의 가치를 제대로 실현시켜주는군요|0 +차 팔아야됨|0 +팩트는 저 박사방놈이 일베인게 팩트로 밝혀졌잖아(일베가입 노무현 대통령 조롱 노알라사진등) 한심한 박사모야|1 +싼타페도 옆에 차 들이받았구만 뭔 싼타페탓하고 난리여|0 +트럼프.재는 쩍팔리지도안냐|1 +도대체 왜 ;;|0 +성심껏 사과하고 배상하고 재발방지 약속하면 조용히 넘어 갈 수 있는 일을 자식새끼 전국적인 호로새끼 만들려고 엄마가 작정을 했구나 자식 인생 망치는 부모의 표본같으니라구|0 +시발 말론브란도래 ㅋㅋㅋㅋ|1 +근데 이건 좆병신 싼티나는 카톡 어플 주작이라 ㅁㅈㅎ|1 +ㅋㅋ|0 +아무 죄없는 청소부한테 자식교육 시킨다고 너 공부안하면 저 아저씨처럼 돼.. 이거랑 같은 경우인데 병신들이 7등급이고 기술이고 머고 헛소리고 ㅋㅋㅋ 저 여자가 틀딱마인드 노동은 천하다 무시하는게 문제지|1 +원빈이 영화 고르는 눈이 그렇게 지리셔서 영화를 못 찍고 계시나 봄 ㅠㅠㅠㅠㅠㅠ|0 +돈 못벌더라도 용접공보다 다른 직업이 낫다는말이지돈이 장땡이면 창녀랑 결혼하지그러노|1 +생각이 올발라서 ㅇㅂ|0 +그 축구 루시안 스킨도 누구 따라했다고 논란났는데 ㅋㅋㅋ|0 +창의력 대장이노|0 +마니야 애국할 시점이닷. 똘마니들 일본으로 긴급 투입 지령내려줘~|1 +내가 볼땐 진심 담긴 사과라기보단 예쁘게 찍히려고 카메라 의식하는거 같노|0 +그건 니가 개보지년이어서 그런거 아닐까?|1 +내가 살아있을때 기생충 구더기 곱등이 연가시 엑시구아 바퀴벌레 시궁창 쥐새끼들한테 애미 애비가 창자까지 돌림빵쳐당한 유사인류 헬짱깨 종이호랑이 에미뒤진 걸레갈보개씹창년 허벌보지 니미 씹창 좆물통을 사시미로 토막내고 도려낸 다음에 마체테로 찢어죽여버릴 육변기 개갈보지 씹창년새끼들 쳐망하는걸 볼수 있다면 그걸로도 감사한거지 ㅇㅇ|1 +너무착해져서 개가 자원봉사다니더라|0 + 그럼 20에 사는게 아니지 ㅋㅋ|0 +주예지가 호주 간다면 용접하러 가는건 아니겠지 물론ㅋ|0 +난 여초회사 다니는데 밥먹을때 부대찌개에 앞접시 안 덜고 지들이 그냥 숟가락 휘휘 저어 먹음 참고로 다들 20대 여직원들임 이거 업계포상이노?|1 +주머니 지퍼색도 같고|0 +블레이드러너에서 일본여성이 기모노입고 대형 광고판에 등장히는 장면 진짜 유명하지 ㅎㅎ저기도 있네 일본 성장에 대한 두려움을 상징한 장면지금은 폭망했지만|0 +존나 잘함 파브레가스 마티치 라인 343 콘동님 시절에 애들 다 쓸어먹음전반기 한정임 근데 ㅋㅋㅋ후반기에는 너무 힘들어하는게 보였음 ㅋㅋ|1 +조선은 그냥 자멸했고건국의 흐름은 6.25를 계기로 나타났다가 성과를 거두자 만주팔이들이 득세하면서 밀려나갔다.차라리 일본의 지배가 가혹하고 잔혹했으면 이후에 건국의 흐름이 강하게 일어났겠지만, 전혀 그렇지도 않았고그래서 6.25가 유일한 동인이었는데 오래가지 못한 거다.다시 자멸하던 조선말로 회귀한거다.|0 +그 정도는 써야지|0 +와...아이디어 좋다...|0 +아무말 대잔치 이건 사년전에 본 단언데 아직도 쓰는새끼가 있냐,,, 니 에미에비가 쓰디? 아 뒤졌냐 ㅋㅋㅋㅋㅋㅋㅋ ㅈㅅ ㅎㅎ|1 +요즘애들한테 옥상 뛰어다닌썰 풀면 틀딱취급 받겠노|1 +보지도 압축해 제공하라|1 +뽕삘인데|0 +돌빵한방에 크랙날수있어서|0 +레알 개읏기노|0 +겨드랑이는 볼 생각도 못했는데나 비정상임?매번 다른 사람들이 겨드랑이 하면 그때 보긴보는데그닥 페티쉬가없는건가..|0 +넌 어디살어?|0 +보자마자 터짐 ㅋ|0 +어우시발꼴려|1 +빨리 구라였다고 둘러치고 효진이 소개시켜달라고 졸라라|1 +아니 이름 뭐냐고 씹아 ㅋㅋ|1 + 12억 이상도 가능하겠네|0 +커엽다ㅋㅋ|0 + 이런 저능아들은 도대체 뭔생각하면서 사는지 모르겠네|1 +연구소는 그런가보네.|0 +당연한거 아니냐? 자기 몸건강 관리도 못하는애들이 용접공이니뭐니 하면 콧방귀 뀔듯|1 +예상 ㅈㅎ 병신아|1 +자기 생명의 위협을 못느끼고 남에게 의지하는 본능이면 저때 안죽었어도 다른데서 죽어서 제명에 못살았을거 같음|0 +아니 남에맘을 왜쳐빠냐|1 +이런걸널리좀 퍼트리자 내가 레몬테라스랑 오유에 먼저 알릴께거기 오랜 회원이었거든|0 +진짜 개극혐 직업이다 ㄹㅇ|1 +빨갱아 북한 핵개발에 대해서 어떻게 생각해?|1 +난 김성령 ~|0 +한국 ㅋㅋㅋ 또 나왔군 ㅋㅋ 이번엔 주예지다이기야!말한마디 잘못했어? 인성이 안됬어? 그럼 모두 달려들어 저년을 죽이라이기야!|1 + 내 주변의 거의 모든 집은 아침에 빵이나 다른걸 먹는 걸로 바뀐지가 한참임|0 +그거 아저씨가 나중이 다시 맛있다고 실토하심 ㅋㅋ|0 +의무와 노력과 희생을 하는 남성들이 오히려 역역차별로 노예가 되었고...|0 +헤드기어 어쩔때 쓰는거고 어쩔때 안쓰는거임 ㅇㅅㅇ??|0 +레알 ㅋㅋ 남자들은 하는 것도 없으면서 키보드로 털기만 잘함 ㅌ|0 +저거 직진 , 후진 가능하다는 표시 아니였음?|0 +너랑 대화하려니 수준이 너무 떨어진다 ㅋ|1 +나도 caltech에서 Ph.D 수료하고 거제 조선소에서 잠수용접 하는대|0 +왜케 비뚤어졌냐?|0 +솔직히 집에서 고기 구우면 좆같은건 사실임 집이 존나 넓르면 모르겠는데.. 암튼 뒷정리하기 개좆같음|1 +공개해야됨|0 +ㅈㅈㅂ ㅁㅈㅎhttps://www.ilbe.com/view/11226188404?c=11226214943#comm_11226214943|0 +친구녀석이 니가 뽑은 오크여사 얘기에 얼마나 열이 받던지...|0 +개신교들의 개념과 숫자를 |0 +암튼 짜게먹으면 몸망치는 지름길임 담배보다 더 안좋다고봄|0 +양반세력 키워서 노비가 주인한테 맞아도 고소 안된다는 인식의 기틀을 쌓으신분임.|0 +다 기관총으로 총살해라|1 +ㅇㅂ하는 화석으로 발견 되겠노|0 +이씨 박씨 수혜 받았네.|0 +개소리는 일기장에 쓰시오!|1 +내 글을 유일하게 이해한 새끼가 나타났네|1 +와 테일윈드가 이러케이뻣나? 잘입으시니까 존나이뻐보이네요|1 +진짜 순발력 센스는 올타임원탑|0 + 단순 용접 기술이 들어가 있다는게 전부 용접으로 햇단 말은 아니 잖아.|0 + 예를들면 조선이 -100 -> 일제가 -50라는 거임. 둘 다 지금처럼 완전히 자유롭지는 못해서 마이너스지만 어찌됐건 조선보단 나았다는 것|0 +시@발 저 효자통닭 조림닭 생각하면 좆같네 걍 안동찜닭 국물없어질떄까찌 조린건데조오오온나 짜고 간도 안베고 맛대가리 존나없어서 생활의달인 어케나온거지 이생각하다가지인중에 안동사람있길래 거기 갔다왓다니까 거기 어딘지도모르고 안동사람들은 안간다더라 ㅋㅋㅋㅋㅋ|1 + 안타깝노|0 +좆같은 짓 그만하라고 옳은 말 하면노잼충 씹선비 홍어 분탕이 되는 으메이징한 싸이트 ㅋㅋㅋ그러면서 욕 안먹고 인간다운 대접을 바란다???애국보수라서 욕먹고 탄압당한다고????? ㅋㅋ아이고~~슨상님 계실적엔 이런일이 읎었는디아이고~~민주주으야~~~아이고~~나가 호남사람이라 부당한 모욕을 당해부러~~~~이거 완전 섬노예 부리는 신안 홍어새끼들 범죄자 마인드 아니냐?|1 +코나파라|0 + 어느 나라에서 유래했는지|0 +위에꺼 3개는 너무 어거지다|0 +식기가없는대어디에밥을주라는거여|0 +저런년은 그냥 뒤져야됨|1 +나랑똑같노|0 +실력도 없고 기초 사실 틀리고 어중간한 인기|1 +또 또 또 일게이들 같은 일게이 조리돌림 해서 컨텐츠 하나 죠졌누....|1 +오프닝만 봐도 존나 웃겼음 ㅋㅋ|1 +안감? 못감|0 +지 장비 일본 스키장에 두고 왔다고 자랑스럽게 떠벌림. ㅋㅋ갔더니 너무너무 좋았다고. 너무 좋아서 다시 가려고 했다고 함. ㅋㅋㅋㅋ|1 +쓰레기 수거를 왜 안하냐 ㅋㅋ 일주일에 한두번이라도 수거해가긴 한다.농작물이나 나무 태우는것 까진 이해해준다쳐도|0 +가급적 그냥 쭈욱 해줘도 된다.|0 +극혐|1 +니그튼 그지새끼 아니더라도 아 배달비 있네??? 아 귀찮다 그냥 사먹어야지등등 이런 수요가 있으니까 존재가 하는거고 배달비 받는 업체들이 더 많이 늘어나는 거다거지새끼들아니랄까봐 자본주의시장에서 수요와 공급에 따라서 흘러가는 시장 흐름을 지들 돈 없다고 찡찡거리는 꼬라지 보소 이딴게 일베 ㅋㅋㅋㅋㅋ돈 아까우면 먹지마 가서 포장 해 오던가니들 같은 거지 아니더라도 먹는 사람 널렸어|1 +나씨 전라도성씨임|0 +궁금|0 +내가널어찌할줄모르니|0 +펨코 이 씨발련들 싸가지 왤케 없냐저 정도 길이면 그냥 건너갈 수도 있지누군가의 엄마이고 할머니이고 그럴텐데댓글 꼬라지 존나 싸가지 없네 이 개쓰레기새끼들이|1 +홍어새끼들 존나 많네 이정도 디테일보고 주작주작 거리고 있네 씨발년들아 태어나기전일을 어떻게 인증을해 애미 쳐뒤졌나 |1 +지방것들이 왜 서울 품평하냐?|0 +손주은 센세가 떠오르노 ㅋㅋㅋㅋㅋㅋㅋ강의시간에 너 공부 안하면 창녀보다 못한 삶을 살게 될거라는 희대의 발언창녀는 화대라도 받는다고, 공부도 못하고 못생긴 얼굴이면 돈주고 결혼해야 한다고|1 +나도 오늘부터 사진작가가 되겠다이기야!|0 +나도 눈.저랬는데속씽수하고 용됨|0 +그러니까 생활연기를 잘할 뿐 진짜 캐릭터 연기를 잘 하는지는 논할 수가 없지|0 +나랑 동갑이네 ㅋㅋ|0 +애초에 비주얼이 저러면 구독자 없어서 무슨 발언을 해도 논란안됨ㅅㄱㅁㅈㅎ 달게받으렴|1 +사실 진정한 승리자는 러블리즈 뿐이지...|0 +짜식.. 아직 일베의 드립을 잘 모르는구나..|0 +뭘 모르나 본데, 머리 길면 여군. 빡빡이는 군인. 알간???|0 +이래서 애새끼 함부로 싸지르는거 아니다 |1 +애미애비도없노 ㅋ|1 + 1. 한국전쟁 당시, 미국 외교 담당 부서인 CFR은 김일성에게 |0 +도쿄 올림픽때만 응윈 깔짝하겠지 ㅠㅠ 복싱에 대한 국민인식이 |0 +응 그래 저능아새끼야 그렇게 생각하고 살렴. 저능아는 저능아대로살아야지 시발 불끄는게 먼저지 병신새끼|1 +둘다 헐겁기는 마찬가지|0 +3행시 놀이..|0 +진짜 저 개성없는 머리는 왜 하는거냐|1 +오나1홀도 애1무 잘해주면 물 나온다 함 해보라ㅏ|1 +좀있으면 100억도 나오겠네 시발 ㅋㅋㅋㅋㅋㅋ|1 +사진작가: 컷! 얼릉 옷입으세요|0 +이쯤되니 전남대 전북대 상황이 궁금해지네 |0 +WIN10 업그레이드 했냐??|0 +좌우 헬파이어 3개씩 6개 장착에 아파치보다 약하지만 20mm 기관포 장착임|0 +세금 내놓으라고 데모질 중 ㅋㅋ간첩, 도둑놈 새끼들이 대통령 해처먹으니까 나도 같이 해먹자며 나라 전체에 도둑놈만 득실거림 ㅋㅋ품종도 도둑질하는 조센진들|1 + 너가 한 말 중에|0 +제갈대중 ㅇㅂ|0 +우리나라교회는 악마들이모여 주뎅이 처벌리고 |1 +나가뒤져|1 +죽어버려|1 +걍 죽어|1 +뒤지는거 추천|1 +자살해라 걍|1 +진지하게 자살하는거 어떰?|1 +나같으면 걍 자살한다|1 \ No newline at end of file diff --git a/ai/model.joblib b/ai/model.joblib new file mode 100644 index 0000000000..3d9c4a90a7 Binary files /dev/null and b/ai/model.joblib differ diff --git a/ai/requirements.txt b/ai/requirements.txt new file mode 100644 index 0000000000..b62b167ee5 --- /dev/null +++ b/ai/requirements.txt @@ -0,0 +1,27 @@ +annotated-types==0.6.0 +anyio==4.3.0 +click==8.1.7 +fastapi==0.110.1 +h11==0.14.0 +idna==3.7 +joblib==1.4.0 +nltk==3.8.1 +numpy==1.26.4 +pandas==2.2.2 +pybind11==2.12.0 +pydantic==2.7.0 +pydantic_core==2.18.1 +python-dateutil==2.9.0.post0 +pytz==2024.1 +regex==2024.4.16 +scikit-learn==1.4.2 +scipy==1.13.0 +setuptools==69.5.0 +six==1.16.0 +sniffio==1.3.1 +starlette==0.37.2 +threadpoolctl==3.4.0 +tqdm==4.66.2 +typing_extensions==4.11.0 +tzdata==2024.1 +uvicorn==0.29.0 diff --git a/ai/sklearn_traning.py b/ai/sklearn_traning.py new file mode 100644 index 0000000000..6da23eb9b1 --- /dev/null +++ b/ai/sklearn_traning.py @@ -0,0 +1,94 @@ +import pandas as pd +import re +from sklearn.feature_extraction.text import TfidfVectorizer +from sklearn.model_selection import train_test_split, GridSearchCV +from sklearn.linear_model import LogisticRegression +from sklearn.metrics import accuracy_score +import random +import nltk +from nltk.corpus import wordnet + +nltk.download('wordnet') +nltk.download('omw-1.4') + +def augment_text(text, num_augment=2): + words = text.split() + augmented_texts = [text] # 원본 텍스트도 포함 + + for _ in range(num_augment): + operation = random.choice(['synonym', 'insert', 'swap', 'delete']) + if operation == 'synonym' and len(words) > 1: + # 랜덤한 단어를 동의어로 교체 + word_to_replace = random.choice(words) + synonyms = [syn.lemmas()[0].name() for syn in wordnet.synsets(word_to_replace) if syn.lemmas()] + if synonyms: + new_word = random.choice(synonyms) + new_text = text.replace(word_to_replace, new_word, 1) + augmented_texts.append(new_text) + elif operation == 'insert' and len(words) > 1: + # 랜덤한 위치에 랜덤 단어의 동의어 삽입 + word_to_insert = random.choice(words) + synonyms = [syn.lemmas()[0].name() for syn in wordnet.synsets(word_to_insert) if syn.lemmas()] + if synonyms: + new_word = random.choice(synonyms) + insert_position = random.randint(0, len(words)) + words.insert(insert_position, new_word) + augmented_texts.append(' '.join(words)) + elif operation == 'swap' and len(words) > 1: + # 두 단어의 위치를 서로 바꿈 + idx1, idx2 = random.sample(range(len(words)), 2) + words[idx1], words[idx2] = words[idx2], words[idx1] + augmented_texts.append(' '.join(words)) + elif operation == 'delete' and len(words) > 1: + # 랜덤하게 한 단어를 삭제 + words.pop(random.randint(0, len(words)-1)) + augmented_texts.append(' '.join(words)) + + return augmented_texts + +def normalize_text(text): + text = re.sub(r"[^\w\s]", "", text) + text = re.sub(r"\s+", " ", text).strip().lower() + return text + +def load_and_preprocess_data(filepath): + data = [] + with open(filepath, 'r', encoding='utf-8') as file: + for line in file: + text, label = line.strip().split('|', maxsplit=1) + normalized_text = normalize_text(text) + label = int(label) + if label == 1: + augmented_texts = augment_text(normalized_text) + data.extend((aug_text, label) for aug_text in augmented_texts) + else: + data.append((normalized_text, label)) + return pd.DataFrame(data, columns=['text', 'label']) + +df = load_and_preprocess_data('dataset.txt') + +tfidf_vectorizer = TfidfVectorizer(analyzer='char', ngram_range=(1,3), max_features=1000) +X = tfidf_vectorizer.fit_transform(df['text']).toarray() +y = df['label'] + +X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) + +param_grid = { + 'C': [0.01, 0.1, 1, 10, 100], + 'max_iter': [100, 500, 1000] +} + +grid_search = GridSearchCV(LogisticRegression(), param_grid, cv=5, scoring='accuracy') +grid_search.fit(X_train, y_train) + +print("Best Parameters: {}".format(grid_search.best_params_)) +y_pred = grid_search.predict(X_test) +accuracy = accuracy_score(y_test, y_pred) +print(f'Test Accuracy: {accuracy:.2f}') + +from joblib import dump +model_bundle = { + "vectorizer": tfidf_vectorizer, + "model": grid_search +} +dump(model_bundle, 'model.joblib') diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000000..f41651948f --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,47 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Kotlin ### +.kotlin + +### application.properties ### +application-security.yml +application-prod.yml + +### MAC ### +.DS_Store \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000000..35446043aa --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,9 @@ +FROM amazoncorretto:17-alpine-jdk + +ARG JAR_FILE=build/libs/backend-0.0.1-SNAPSHOT.jar + +ADD ${JAR_FILE} backend.jar + +ENV TZ=Asia/Seoul + +ENTRYPOINT ["java", "-jar","-Dspring.profiles.active=prod","/backend.jar"] diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts new file mode 100644 index 0000000000..4a298c0e57 --- /dev/null +++ b/backend/build.gradle.kts @@ -0,0 +1,89 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + val kotlinVersion = "1.9.22" + id("org.springframework.boot") version "3.2.3" + id("io.spring.dependency-management") version "1.1.4" + kotlin("jvm") version kotlinVersion + kotlin("plugin.spring") version kotlinVersion + kotlin("plugin.jpa") version kotlinVersion +} + +group = "com.dclass" +version = "0.0.1-SNAPSHOT" + +java { + sourceCompatibility = JavaVersion.VERSION_17 +} + +repositories { + mavenCentral() + maven(url = "https://oss.sonatype.org/content/repositories/snapshots") +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.springframework.boot:spring-boot-starter-thymeleaf") + + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.jetbrains.kotlin:kotlin-reflect") + + implementation("com.linecorp.kotlin-jdsl:jpql-dsl:3.3.1") + implementation("com.linecorp.kotlin-jdsl:jpql-render:3.3.1") + implementation("com.linecorp.kotlin-jdsl:spring-data-jpa-support:3.3.1") + + implementation("aws.sdk.kotlin:ses:1.0.30") + implementation("aws.sdk.kotlin:s3:1.0.30") + implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.11") + + compileOnly("io.jsonwebtoken:jjwt-api:0.11.2") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2") +// implementation("com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0") + + //flyway + implementation("org.flywaydb:flyway-core") + implementation("org.flywaydb:flyway-mysql") + + //swagger + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0") + + implementation("org.springframework.retry:spring-retry") + + runtimeOnly("com.mysql:mysql-connector-j") + runtimeOnly("com.h2database:h2") + + testImplementation("org.springframework.boot:spring-boot-starter-test") { + exclude(group = "org.junit.vintage", module = "junit-vintage-engine") + exclude(group = "org.mockito") + } + testImplementation("com.ninja-squad:springmockk:4.0.2") + testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc") + testImplementation("io.kotest:kotest-runner-junit5:5.4.2") + testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.2") +} + +tasks.withType { + kotlinOptions { + freeCompilerArgs += "-Xjsr305=strict" + jvmTarget = "17" + } +} + +tasks.withType { + useJUnitPlatform() +} + +tasks { + val copySecret by register("copySecret") { + from("./dclass-secret") // 서브모듈 디렉토리 경로 + include("application*.yml") // 복사할 파일들 + into("./src/main/resources") // 복사 위치 + } + + named("processResources") { + dependsOn(copySecret) + } +} \ No newline at end of file diff --git a/backend/dclass-secret b/backend/dclass-secret new file mode 160000 index 0000000000..0c6099070a --- /dev/null +++ b/backend/dclass-secret @@ -0,0 +1 @@ +Subproject commit 0c6099070a146aef5a0d3db3ce5831ee1d275878 diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000000..48a571b84f --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,14 @@ +services: + db: + image: library/mysql:latest + platform: linux/x86_64 + container_name: dclass + restart: always + ports: + - "53306:3306" + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: dclass + MYSQL_USER: dclass-user + MYSQL_PASSWORD: password + TZ: Asia/Seoul \ No newline at end of file diff --git a/backend/gradle/wrapper/gradle-wrapper.jar b/backend/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..d64cd49177 Binary files /dev/null and b/backend/gradle/wrapper/gradle-wrapper.jar differ diff --git a/backend/gradle/wrapper/gradle-wrapper.properties b/backend/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..1af9e0930b --- /dev/null +++ b/backend/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/backend/gradlew b/backend/gradlew new file mode 100755 index 0000000000..1aa94a4269 --- /dev/null +++ b/backend/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/backend/gradlew.bat b/backend/gradlew.bat new file mode 100644 index 0000000000..93e3f59f13 --- /dev/null +++ b/backend/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/backend/settings.gradle.kts b/backend/settings.gradle.kts new file mode 100644 index 0000000000..2401f0a432 --- /dev/null +++ b/backend/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "backend" diff --git a/backend/src/main/kotlin/com/dclass/backend/BackendApplication.kt b/backend/src/main/kotlin/com/dclass/backend/BackendApplication.kt new file mode 100644 index 0000000000..be8aac02a9 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/BackendApplication.kt @@ -0,0 +1,20 @@ +package com.dclass.backend + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.ConfigurationPropertiesScan +import org.springframework.boot.runApplication +import org.springframework.retry.annotation.EnableRetry +import org.springframework.scheduling.annotation.EnableScheduling + + +@SpringBootApplication +@ConfigurationPropertiesScan +@EnableScheduling +@EnableRetry +class BackendApplication + +/**/ + +fun main(args: Array) { + runApplication(*args) +} diff --git a/backend/src/main/kotlin/com/dclass/backend/application/BelongService.kt b/backend/src/main/kotlin/com/dclass/backend/application/BelongService.kt new file mode 100644 index 0000000000..2f1ac6cccd --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/BelongService.kt @@ -0,0 +1,40 @@ +package com.dclass.backend.application + +import com.dclass.backend.application.dto.RemainDurationResponse +import com.dclass.backend.application.dto.SwitchDepartmentResponse +import com.dclass.backend.application.dto.UpdateDepartmentRequest +import com.dclass.backend.domain.belong.BelongRepository +import com.dclass.backend.domain.belong.getOrThrow +import com.dclass.backend.domain.department.DepartmentRepository +import com.dclass.backend.domain.department.getByIdOrThrow +import com.dclass.backend.domain.department.getByTitleOrThrow +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Transactional +@Service +class BelongService( + private val departmentRepository: DepartmentRepository, + private val belongRepository: BelongRepository, +) { + fun editDepartments(userId: Long, request: UpdateDepartmentRequest) { + val majorDepartment = departmentRepository.getByTitleOrThrow(request.major) + val minorDepartment = departmentRepository.getByTitleOrThrow(request.minor) + + val belong = belongRepository.getOrThrow(userId) + belong.update(listOf(majorDepartment.id, minorDepartment.id)) + } + + fun switchDepartment(userId: Long): SwitchDepartmentResponse { + val belong = belongRepository.getOrThrow(userId) + belong.switch() + + return departmentRepository.getByIdOrThrow(belong.activated).title + .let(::SwitchDepartmentResponse) + } + + fun remain(userId: Long): RemainDurationResponse { + val belong = belongRepository.getOrThrow(userId) + return RemainDurationResponse(belong.remainingTime) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/BlacklistService.kt b/backend/src/main/kotlin/com/dclass/backend/application/BlacklistService.kt new file mode 100644 index 0000000000..c3a0bd8966 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/BlacklistService.kt @@ -0,0 +1,31 @@ +package com.dclass.backend.application + +import com.dclass.backend.application.dto.LoginUserResponse +import com.dclass.backend.domain.blacklist.Blacklist +import com.dclass.backend.domain.blacklist.BlacklistRepository +import com.dclass.backend.exception.blacklist.BlacklistException +import com.dclass.backend.exception.blacklist.BlacklistExceptionType.ALREADY_LOGOUT +import com.dclass.backend.security.JwtTokenProvider +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + + +@Transactional +@Service +class BlacklistService( + private val blacklistRepository: BlacklistRepository, + private val jwtTokenProvider: JwtTokenProvider, +) { + fun reissueToken(refreshToken: String): LoginUserResponse { + jwtTokenProvider.validateToken(refreshToken) + blacklistRepository.findByInvalidRefreshToken(refreshToken) + ?.let { throw BlacklistException(ALREADY_LOGOUT) } + ?: blacklistRepository.save(Blacklist(refreshToken)) + + val email = jwtTokenProvider.getSubject(refreshToken) + return LoginUserResponse( + jwtTokenProvider.createAccessToken(email), + jwtTokenProvider.createRefreshToken(email) + ) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/BlocklistService.kt b/backend/src/main/kotlin/com/dclass/backend/application/BlocklistService.kt new file mode 100644 index 0000000000..d805837566 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/BlocklistService.kt @@ -0,0 +1,18 @@ +package com.dclass.backend.application + +import com.dclass.backend.application.dto.RemainDurationResponse +import com.dclass.backend.domain.blocklist.BlocklistRepository +import com.dclass.backend.domain.blocklist.getLatestByUserIdOrThrow +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Transactional +@Service +class BlocklistService( + private val blocklistRepository: BlocklistRepository +) { + fun remain(userId: Long): RemainDurationResponse { + val blocklist = blocklistRepository.getLatestByUserIdOrThrow(userId) + return RemainDurationResponse(blocklist.remainingTime) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/CommentService.kt b/backend/src/main/kotlin/com/dclass/backend/application/CommentService.kt new file mode 100644 index 0000000000..5f9ccbc297 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/CommentService.kt @@ -0,0 +1,110 @@ +package com.dclass.backend.application + +import com.dclass.backend.application.dto.* +import com.dclass.backend.domain.comment.CommentRepository +import com.dclass.backend.domain.comment.getByIdOrThrow +import com.dclass.backend.domain.community.CommunityRepository +import com.dclass.backend.domain.community.findByIdOrThrow +import com.dclass.backend.domain.notification.NotificationEvent +import com.dclass.backend.domain.post.PostRepository +import com.dclass.backend.domain.post.findByIdOrThrow +import com.dclass.backend.domain.reply.ReplyRepository +import com.dclass.backend.domain.userblock.UserBlockRepository +import com.dclass.backend.exception.comment.CommentException +import com.dclass.backend.exception.comment.CommentExceptionType +import org.springframework.context.ApplicationEventPublisher +import org.springframework.orm.ObjectOptimisticLockingFailureException +import org.springframework.retry.annotation.Backoff +import org.springframework.retry.annotation.Retryable +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Transactional +@Service +class CommentService( + private val commentRepository: CommentRepository, + private val replyRepository: ReplyRepository, + private val postRepository: PostRepository, + private val commentValidator: CommentValidator, + private val communityRepository: CommunityRepository, + private val eventPublisher: ApplicationEventPublisher, + private val userBlockRepository: UserBlockRepository +) { + @Retryable( + ObjectOptimisticLockingFailureException::class, + maxAttempts = 3, + backoff = Backoff(delay = 500) + ) + fun create(userId: Long, request: CreateCommentRequest): CommentResponse { + val post = postRepository.findByIdOrThrow(request.postId) + val community = communityRepository.findByIdOrThrow(post.communityId) + commentValidator.validate(userId, community) + val comment = commentRepository.save(request.toEntity(userId)) + + post.increaseCommentReplyCount() + + if (post.isEligibleForSSE(userId)) { + val event = NotificationEvent.commentToPostUser(post, comment, community) + eventPublisher.publishEvent(event) + } + return CommentResponse(comment) + } + + fun update(userId: Long, request: UpdateCommentRequest) { + val comment = commentRepository.findByIdAndUserId(request.commentId, userId) + ?: throw CommentException(CommentExceptionType.NOT_FOUND_COMMENT) + + comment.changeContent(request.content) + } + + @Retryable( + ObjectOptimisticLockingFailureException::class, + maxAttempts = 3, + backoff = Backoff(delay = 500) + ) + fun delete(userId: Long, request: DeleteCommentRequest) { + val comment = commentRepository.findByIdAndUserId(request.commentId, userId) + ?: throw CommentException(CommentExceptionType.NOT_FOUND_COMMENT) + if(comment.isDeleted()) throw CommentException(CommentExceptionType.DELETED_COMMENT) + commentRepository.delete(comment) + + val post = postRepository.findByIdOrThrow(comment.postId) + post.decreaseCommentReplyCount() + } + + fun like(userId: Long, request: LikeCommentRequest) { + val comment = commentRepository.getByIdOrThrow(request.commentId) + val post = postRepository.findByIdOrThrow(comment.postId) + val community = communityRepository.findByIdOrThrow(post.communityId) + + commentValidator.validate(userId, community) + comment.like(userId) + } + + @Transactional(readOnly = true) + fun findAllByPostId(userId: Long, request: CommentScrollPageRequest): CommentsResponse { + val comments = commentRepository.findCommentWithUserByPostId(request) + val blockedUserIds = + userBlockRepository.findByBlockerUserId(userId).associateBy { it.blockedUserId } + + comments.forEach { + it.isLiked = it.likeCount.findUserById(userId) + it.isBlockedUser = blockedUserIds.contains(it.userId) + } + + val commentIds = comments.map { it.id } + + val replies = replyRepository.findRepliesWithUserByCommentIdIn(commentIds) + .onEach { it.isBlockedUser = blockedUserIds.contains(it.userId) } + .groupBy { it.commentId } + + + val data = comments.map { + CommentReplyWithUserResponse( + it, + replies = replies[it.id] ?: emptyList() + ) + } + return CommentsResponse.of(data, request.size) + } +} diff --git a/backend/src/main/kotlin/com/dclass/backend/application/CommentValidator.kt b/backend/src/main/kotlin/com/dclass/backend/application/CommentValidator.kt new file mode 100644 index 0000000000..d5d82957e7 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/CommentValidator.kt @@ -0,0 +1,27 @@ +package com.dclass.backend.application + +import com.dclass.backend.domain.belong.BelongRepository +import com.dclass.backend.domain.belong.getOrThrow +import com.dclass.backend.domain.blocklist.BlocklistRepository +import com.dclass.backend.domain.community.Community +import com.dclass.backend.exception.comment.CommentException +import com.dclass.backend.exception.comment.CommentExceptionType.FORBIDDEN_COMMENT +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional +class CommentValidator( + private val belongRepository: BelongRepository, + private val blocklistRepository: BlocklistRepository +) { + fun validate(userId: Long, community: Community) { + blocklistRepository.findFirstByUserIdOrderByCreatedDateTimeDesc(userId)?.validate() + + val belong = belongRepository.getOrThrow(userId) + + if (!belong.contain(community.departmentId)) { + throw CommentException(FORBIDDEN_COMMENT) + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/NotificationService.kt b/backend/src/main/kotlin/com/dclass/backend/application/NotificationService.kt new file mode 100644 index 0000000000..2b2a7f7e9f --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/NotificationService.kt @@ -0,0 +1,113 @@ +package com.dclass.backend.application + +import com.dclass.backend.application.dto.NotificationRequest +import com.dclass.backend.domain.emitter.EmitterRepository +import com.dclass.backend.domain.notification.NotificationRepository +import com.dclass.backend.domain.notification.getOrThrow +import com.dclass.support.util.logger +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter + +@Service +@Transactional +class NotificationService( + val emitterRepository: EmitterRepository, + val notificationRepository: NotificationRepository, +) { + // + private val DEFAULT_TIMEOUT = 1000L * 60L * 60L; + private val log = logger() + + + fun subscribe(id: Long, lastEventId: String): SseEmitter { + val emitterId = makeTimeIncludeId(id) + val emitter = emitterRepository.save(emitterId, SseEmitter(DEFAULT_TIMEOUT)) + emitter.onTimeout(emitter::complete) + emitter.onCompletion { emitterRepository.delete(emitterId) } + + sendNotification( + emitter, + makeTimeIncludeId(id), + emitterId, + "EventStream Created. [userId=$id]" + ) + + if (hasLostData(lastEventId)) { + sendLostData(emitter, lastEventId, emitterId, id) + } + + return emitter; + } + + + fun send(request: NotificationRequest) { + val notification = notificationRepository.save(request.toEntity()) + + val eventId = makeTimeIncludeId(request.userId) + val emitters = emitterRepository.findAllEmitterStartWithByUserId(request.userId.toString()) + emitters.forEach { + val response = request.createResponse(notification.id, notification.createdAt) + emitterRepository.saveEventCache(it.key, notification) + sendNotification(it.value, eventId, it.key, response) + } + } + + @Scheduled(fixedRate = 1000 * 60 * 3) + fun sendHeartbeat() { + val emitters = emitterRepository.findAll() + emitters.forEach { + sendNotification(it.value, it.key.split("_")[0], it.key, "heartbeat") + } + } + + fun readNotification(id: Long) { + val notification = notificationRepository.getOrThrow(id) + notification.read() + } + + private fun makeTimeIncludeId(id: Long): String { + return "${id}_${System.currentTimeMillis()}" + } + + private fun sendNotification( + emitter: SseEmitter, + eventId: String, + emitterId: String, + data: Any + ) { + try { + emitter.send( + SseEmitter.event() + .id(eventId) + .name(emitterId) + .data(data) + ) + log.info("Notification sent. data=$data, emitterId=$emitterId") + } catch (e: Exception) { + log.error( + "Error occurred while sending notification. [emitterId=$emitterId]", + e.message + ) + emitter.completeWithError(e) + } + } + + private fun hasLostData(lastEventId: String): Boolean { + return lastEventId.isNotEmpty() + } + + private fun sendLostData( + emitter: SseEmitter, + lastEventId: String, + emitterId: String, + userId: Long + ) { + val eventCache = emitterRepository.findAllEventCacheStartWithByUserId(userId.toString()) + eventCache.filter { it.key > lastEventId }.forEach { + sendNotification(emitter, it.key, emitterId, it.value) + } + } + +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/PasswordGenerator.kt b/backend/src/main/kotlin/com/dclass/backend/application/PasswordGenerator.kt new file mode 100644 index 0000000000..46504f6166 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/PasswordGenerator.kt @@ -0,0 +1,5 @@ +package com.dclass.backend.application + +interface PasswordGenerator { + fun generate(): String +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/PostService.kt b/backend/src/main/kotlin/com/dclass/backend/application/PostService.kt new file mode 100644 index 0000000000..21624501dc --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/PostService.kt @@ -0,0 +1,166 @@ +package com.dclass.backend.application + +import com.dclass.backend.application.dto.* +import com.dclass.backend.domain.belong.BelongRepository +import com.dclass.backend.domain.belong.getOrThrow +import com.dclass.backend.domain.community.CommunityRepository +import com.dclass.backend.domain.post.PostRepository +import com.dclass.backend.domain.post.findByIdOrThrow +import com.dclass.backend.domain.scrap.ScrapRepository +import com.dclass.backend.domain.userblock.UserBlockRepository +import com.dclass.backend.exception.post.PostException +import com.dclass.backend.exception.post.PostExceptionType +import com.dclass.backend.exception.post.PostExceptionType.NOT_FOUND_POST +import com.dclass.backend.infra.s3.AwsPresigner +import com.dclass.support.domain.Image +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime + +@Service +@Transactional +class PostService( + private val postValidator: PostValidator, + private val postRepository: PostRepository, + private val belongRepository: BelongRepository, + private val communityRepository: CommunityRepository, + private val scrapRepository: ScrapRepository, + private val userBlockRepository: UserBlockRepository, + private val awsPresigner: AwsPresigner +) { + fun getAll(userId: Long, request: PostScrollPageRequest): PostsResponse { + val activatedDepartmentId = belongRepository.getOrThrow(userId).activated + val communityIds = communityRepository.findByDepartmentId(activatedDepartmentId) + .map { it.id } + + val posts = postRepository.findPostScrollPage(communityIds, request).onEach { + it.images = runBlocking(Dispatchers.IO) { + it.images.map { async { awsPresigner.getPostObjectPresigned(it) } }.awaitAll() + } + blockedUsers(userId, it) + } + + return PostsResponse.of(posts, request.size) + } + + fun getByUserId(userId: Long, request: PostScrollPageRequest): PostsResponse { + val posts = postRepository.findPostScrollPageByUserId(userId, request).onEach { + it.images = runBlocking(Dispatchers.IO) { + it.images.map { async { awsPresigner.getPostObjectPresigned(it) } }.awaitAll() + } + blockedUsers(userId, it) + } + + return PostsResponse.of(posts, request.size) + } + + fun getScrapped(userId: Long, request: PostScrollPageRequest): PostsResponse { + val posts = postRepository.findScrapPostByUserId(userId).onEach { + it.images = runBlocking(Dispatchers.IO) { + it.images.map { async { awsPresigner.getPostObjectPresigned(it) } }.awaitAll() + } + blockedUsers(userId, it) + } + + return PostsResponse.of(posts, request.size) + } + + fun getCommentedAndReplied(userId: Long, request: PostScrollPageRequest): PostsResponse { + val posts = postRepository.findCommentedAndRepliedPostByUserId(userId, request).onEach { + it.images = runBlocking(Dispatchers.IO) { + it.images.map { async { awsPresigner.getPostObjectPresigned(it) } }.awaitAll() + } + blockedUsers(userId, it) + } + + return PostsResponse.of(posts, request.size) + } + + fun getById(userId: Long, postId: Long): PostDetailResponse { + postValidator.validate(userId, postId) + + val post = postRepository.findPostById(postId).apply { + images = runBlocking(Dispatchers.IO) { + images.map { async { awsPresigner.getPostObjectPresigned(it) } }.awaitAll() + } + blockedUsers(userId, this) + } + + val likePost = postRepository.findByIdOrThrow(postId) + + post.isScrapped = scrapRepository.existsByUserIdAndPostId(userId, postId) + post.likedBy = likePost.likedBy(userId) + + return post + } + + fun create(userId: Long, request: CreatePostRequest): PostDetailResponse { + val community = postValidator.validate(userId, request.communityTitle) + + validatePostTerm(userId) + + val post = postRepository.save(request.toEntity(userId, community.id)) + + return postRepository.findPostById(post.id).apply { + images = runBlocking(Dispatchers.IO) { + images.map { async { awsPresigner.putPostObjectPresigned(it) } }.awaitAll() + } + } + } + + fun update(userId: Long, request: UpdatePostRequest): PostDetailResponse { + val community = postValidator.validate(userId, request.communityTitle) + + val post = postRepository.findByIdAndUserId(request.postId, userId) + ?: throw PostException(NOT_FOUND_POST) + + post.update(request.title, request.content, request.images.map { Image(it) }, community.id) + + val postResponse = postRepository.findPostById(post.id).apply { + images = runBlocking(Dispatchers.IO) { + images.map { async { awsPresigner.putPostObjectPresigned(it) } }.awaitAll() + } + } + + postResponse.isScrapped = scrapRepository.existsByUserIdAndPostId(userId, post.id) + + return postResponse + } + + fun delete(userId: Long, request: DeletePostRequest) { + val post = postRepository.findByIdAndUserId(request.postId, userId) + ?: throw PostException(NOT_FOUND_POST) + postRepository.delete(post) + } + + fun likes(userId: Long, postId: Long): Int { + postValidator.validate(userId, postId) + + val post = postRepository.findByIdOrThrow(postId) + post.addLike(userId) + + return post.postLikesCount + } + + private fun blockedUsers(userId: Long, postResponse: PostResponse) { + val blockedUserIds = userBlockRepository.findByBlockerUserId(userId).associateBy { it.blockedUserId } + postResponse.isBlockedUser = blockedUserIds.contains(postResponse.userId) + } + + private fun blockedUsers(userId: Long, postResponse: PostDetailResponse) { + val blockedUserIds = userBlockRepository.findByBlockerUserId(userId).associateBy { it.blockedUserId } + postResponse.isBlockedUser = blockedUserIds.contains(postResponse.userId) + } + + private fun validatePostTerm(userId: Long) { + val createdDateTime = postRepository.findFirstCreatedDateTimeByUserId(userId) + if (createdDateTime != null && createdDateTime.isAfter(LocalDateTime.now().minusMinutes(1))) { + throw PostException(PostExceptionType.POST_DELAY) + } + + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/PostValidator.kt b/backend/src/main/kotlin/com/dclass/backend/application/PostValidator.kt new file mode 100644 index 0000000000..9e1e3d9f74 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/PostValidator.kt @@ -0,0 +1,48 @@ +package com.dclass.backend.application + +import com.dclass.backend.domain.belong.BelongRepository +import com.dclass.backend.domain.belong.getOrThrow +import com.dclass.backend.domain.blocklist.BlocklistRepository +import com.dclass.backend.domain.community.Community +import com.dclass.backend.domain.community.CommunityRepository +import com.dclass.backend.domain.community.findByIdOrThrow +import com.dclass.backend.domain.post.PostRepository +import com.dclass.backend.domain.post.findByIdOrThrow +import com.dclass.backend.exception.post.PostException +import com.dclass.backend.exception.post.PostExceptionType.FORBIDDEN_POST +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional +class PostValidator( + private val belongRepository: BelongRepository, + private val communityRepository: CommunityRepository, + private val postRepository: PostRepository, + private val blocklistRepository: BlocklistRepository +) { + fun validate(userId: Long, communityTitle: String): Community { + blocklistRepository.findFirstByUserIdOrderByCreatedDateTimeDesc(userId)?.validate() + + val belong = belongRepository.getOrThrow(userId) + val community = + communityRepository.findByDepartmentIdAndTitle(belong.activated, communityTitle) + ?: throw PostException(FORBIDDEN_POST) + + + if (!belong.contain(community.departmentId)) { + throw PostException(FORBIDDEN_POST) + } + return community + } + + fun validate(userId: Long, postId: Long) { + val belong = belongRepository.getOrThrow(userId) + val post = postRepository.findByIdOrThrow(postId) + val community = communityRepository.findByIdOrThrow(post.communityId) + + if (!belong.contain(community.departmentId)) { + throw PostException(FORBIDDEN_POST) + } + } +} diff --git a/backend/src/main/kotlin/com/dclass/backend/application/ReplyService.kt b/backend/src/main/kotlin/com/dclass/backend/application/ReplyService.kt new file mode 100644 index 0000000000..63a81c1c71 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/ReplyService.kt @@ -0,0 +1,98 @@ +package com.dclass.backend.application + +import com.dclass.backend.application.dto.* +import com.dclass.backend.domain.comment.CommentRepository +import com.dclass.backend.domain.comment.getByIdOrThrow +import com.dclass.backend.domain.community.CommunityRepository +import com.dclass.backend.domain.community.findByIdOrThrow +import com.dclass.backend.domain.notification.NotificationEvent +import com.dclass.backend.domain.post.PostRepository +import com.dclass.backend.domain.post.findByIdOrThrow +import com.dclass.backend.domain.reply.ReplyRepository +import com.dclass.backend.domain.reply.getByIdAndUserIdOrThrow +import com.dclass.backend.domain.reply.getByIdOrThrow +import com.dclass.backend.exception.comment.CommentException +import com.dclass.backend.exception.comment.CommentExceptionType +import org.springframework.context.ApplicationEventPublisher +import org.springframework.orm.ObjectOptimisticLockingFailureException +import org.springframework.retry.annotation.Backoff +import org.springframework.retry.annotation.Retryable +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Transactional +@Service +class ReplyService( + private val replyRepository: ReplyRepository, + private val replyValidator: ReplyValidator, + private val eventPublisher: ApplicationEventPublisher, + private val communityRepository: CommunityRepository, + private val commentRepository: CommentRepository, + private val postRepository: PostRepository +) { + @Retryable( + ObjectOptimisticLockingFailureException::class, + maxAttempts = 3, + backoff = Backoff(delay = 500) + ) + fun create(userId: Long, request: CreateReplyRequest): ReplyResponse { + val comment = commentRepository.getByIdOrThrow(request.commentId) + + if (comment.isDeleted()) { + throw CommentException(CommentExceptionType.DELETED_COMMENT) + } + + val post = postRepository.findByIdOrThrow(comment.postId) + val community = communityRepository.findByIdOrThrow(post.communityId) + + replyValidator.validate(userId, community.departmentId) + + val reply = replyRepository.save(request.toEntity(userId)) + + if (post.isEligibleForSSE(userId)) { + val event = NotificationEvent.replyToPostUser(post, comment, reply, community) + eventPublisher.publishEvent(event) + } + if (comment.isEligibleForSSE(userId)) { + val event = NotificationEvent.replyToCommentUser(post, comment, reply, community) + eventPublisher.publishEvent(event) + } + + comment.increaseReplyCount() + post.increaseCommentReplyCount() + + return ReplyResponse(reply) + + } + + fun update(userId: Long, request: UpdateReplyRequest) { + val reply = replyRepository.getByIdAndUserIdOrThrow(request.replyId, userId) + reply.changeContent(request.content) + } + + @Retryable( + ObjectOptimisticLockingFailureException::class, + maxAttempts = 3, + backoff = Backoff(delay = 500) + ) + fun delete(userId: Long, request: DeleteReplyRequest) { + val reply = replyRepository.getByIdAndUserIdOrThrow(request.replyId, userId) + + val comment = commentRepository.getByIdOrThrow(reply.commentId) + val post = postRepository.findByIdOrThrow(comment.postId) + post.decreaseCommentReplyCount() + comment.decreaseReplyCount() + + replyRepository.delete(reply) + } + + fun like(userId: Long, request: LikeReplyRequest) { + val reply = replyRepository.getByIdOrThrow(request.replyId) + val comment = commentRepository.getByIdOrThrow(reply.commentId) + val post = postRepository.findByIdOrThrow(comment.postId) + val community = communityRepository.findByIdOrThrow(post.communityId) + + replyValidator.validate(userId, community.departmentId) + reply.like(userId) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/ReplyValidator.kt b/backend/src/main/kotlin/com/dclass/backend/application/ReplyValidator.kt new file mode 100644 index 0000000000..3b1bbc136b --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/ReplyValidator.kt @@ -0,0 +1,26 @@ +package com.dclass.backend.application + +import com.dclass.backend.domain.belong.BelongRepository +import com.dclass.backend.domain.belong.getOrThrow +import com.dclass.backend.domain.blocklist.BlocklistRepository +import com.dclass.backend.exception.reply.ReplyException +import com.dclass.backend.exception.reply.ReplyExceptionType +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional +class ReplyValidator( + private val belongRepository: BelongRepository, + private val blocklistRepository: BlocklistRepository +) { + fun validate(userId: Long, departmentId: Long) { + blocklistRepository.findFirstByUserIdOrderByCreatedDateTimeDesc(userId)?.validate() + + val belong = belongRepository.getOrThrow(userId) + + if (!belong.contain(departmentId)) { + throw ReplyException(ReplyExceptionType.FORBIDDEN_REPLY) + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/ReportListenerService.kt b/backend/src/main/kotlin/com/dclass/backend/application/ReportListenerService.kt new file mode 100644 index 0000000000..3ebc6cd0e4 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/ReportListenerService.kt @@ -0,0 +1,58 @@ +package com.dclass.backend.application + +import com.dclass.backend.domain.blocklist.Blocklist +import com.dclass.backend.domain.blocklist.BlocklistRepository +import com.dclass.backend.domain.comment.CommentRepository +import com.dclass.backend.domain.comment.getByIdOrThrow +import com.dclass.backend.domain.post.PostRepository +import com.dclass.backend.domain.post.findByIdOrThrow +import com.dclass.backend.domain.reply.ReplyRepository +import com.dclass.backend.domain.reply.getByIdOrThrow +import com.dclass.backend.domain.report.CommentReportedEvent +import com.dclass.backend.domain.report.PostReportedEvent +import com.dclass.backend.domain.report.ReplyReportedEvent +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional +import org.springframework.transaction.event.TransactionalEventListener + + +@Service +@Transactional +class ReportListenerService( + private val postRepository: PostRepository, + private val commentRepository: CommentRepository, + private val replyRepository: ReplyRepository, + private val blocklistRepository: BlocklistRepository +) { + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener + fun delete(event: PostReportedEvent) { + val post = postRepository.findByIdOrThrow(event.postId) + blocklistRepository.save(Blocklist(post.userId)) + postRepository.delete(post) + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener + fun delete(event: CommentReportedEvent) { + val comment = commentRepository.getByIdOrThrow(event.commentId) + val post = postRepository.findByIdOrThrow(comment.postId) + post.decreaseCommentReplyCount() + blocklistRepository.save(Blocklist(comment.userId)) + commentRepository.delete(comment) + + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener + fun delete(event: ReplyReportedEvent) { + val reply = replyRepository.getByIdOrThrow(event.replyId) + val comment = commentRepository.getByIdOrThrow(reply.commentId) + val post = postRepository.findByIdOrThrow(comment.postId) + post.decreaseCommentReplyCount() + blocklistRepository.save(Blocklist(reply.userId)) + replyRepository.delete(reply) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/ReportService.kt b/backend/src/main/kotlin/com/dclass/backend/application/ReportService.kt new file mode 100644 index 0000000000..20a68e7f80 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/ReportService.kt @@ -0,0 +1,31 @@ +package com.dclass.backend.application + +import com.dclass.backend.application.dto.UpdateReportRequest +import com.dclass.backend.domain.report.ReportEvent +import com.dclass.backend.domain.report.ReportRepository +import jakarta.transaction.Transactional +import org.springframework.context.ApplicationEventPublisher +import org.springframework.stereotype.Service + +@Transactional +@Service +class ReportService( + private val reportRepository: ReportRepository, + private val eventPublisher: ApplicationEventPublisher +) { + fun report(userId: Long, request: UpdateReportRequest) { + reportRepository.findByReporterIdAndAndReportedObjectId(userId, request.reportedObjectId)?.let { + reportRepository.delete(it) + } + reportRepository.save(request.toEntity(userId)) + + removeIfReportAccumulated(request) + } + + private fun removeIfReportAccumulated(request: UpdateReportRequest) { + val reportCount = reportRepository.countReportById(request.reportedObjectId, request.reportType) + if (reportCount >= 10) { + eventPublisher.publishEvent(ReportEvent.create(request.reportedObjectId, request.reportType)) + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/ScrapService.kt b/backend/src/main/kotlin/com/dclass/backend/application/ScrapService.kt new file mode 100644 index 0000000000..cdef23554e --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/ScrapService.kt @@ -0,0 +1,41 @@ +package com.dclass.backend.application + +import com.dclass.backend.application.dto.PostResponse +import com.dclass.backend.domain.post.PostRepository +import com.dclass.backend.domain.post.findByIdOrThrow +import com.dclass.backend.domain.scrap.Scrap +import com.dclass.backend.domain.scrap.ScrapRepository +import com.dclass.backend.exception.scrap.ScrapException +import com.dclass.backend.exception.scrap.ScrapExceptionType.NOT_FOUND_SCRAP +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Transactional +@Service +class ScrapService( + private val scrapRepository: ScrapRepository, + private val postRepository: PostRepository, + private val validator: ScrapValidator +) { + + fun create(userId: Long, postId: Long) { + val post = validator.validateScrapPost(userId, postId) + post.increaseScrapCount() + + scrapRepository.save(Scrap(userId, postId)) + } + + fun delete(userId: Long, postId: Long) { + val scrap = scrapRepository.findByUserIdAndPostId(userId, postId) ?: throw ScrapException( + NOT_FOUND_SCRAP + ) + val post = postRepository.findByIdOrThrow(postId) + post.decreaseScrapCount() + + scrapRepository.delete(scrap) + } + + fun getAll(userId: Long): List { + return postRepository.findScrapPostByUserId(userId) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/ScrapValidator.kt b/backend/src/main/kotlin/com/dclass/backend/application/ScrapValidator.kt new file mode 100644 index 0000000000..dfe4e3fa4e --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/ScrapValidator.kt @@ -0,0 +1,39 @@ +package com.dclass.backend.application + +import com.dclass.backend.domain.belong.BelongRepository +import com.dclass.backend.domain.belong.getOrThrow +import com.dclass.backend.domain.community.CommunityRepository +import com.dclass.backend.domain.community.findByIdOrThrow +import com.dclass.backend.domain.post.Post +import com.dclass.backend.domain.post.PostRepository +import com.dclass.backend.domain.post.findByIdOrThrow +import com.dclass.backend.domain.scrap.ScrapRepository +import com.dclass.backend.exception.scrap.ScrapException +import com.dclass.backend.exception.scrap.ScrapExceptionType.ALREADY_SCRAP_POST +import com.dclass.backend.exception.scrap.ScrapExceptionType.PERMISSION_DENIED +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Transactional +@Service +class ScrapValidator( + private val scrapRepository: ScrapRepository, + private val belongRepository: BelongRepository, + private val postRepository: PostRepository, + private val communityRepository: CommunityRepository +) { + fun validateScrapPost(userId: Long, postId: Long): Post { + if (scrapRepository.existsByUserIdAndPostId(userId, postId)) { + throw ScrapException(ALREADY_SCRAP_POST) + } + val belong = belongRepository.getOrThrow(userId) + val post = postRepository.findByIdOrThrow(postId) + val community = communityRepository.findByIdOrThrow(post.communityId) + + if (!belong.contain(community.departmentId)) { + throw ScrapException(PERMISSION_DENIED) + } + return post + } +} + diff --git a/backend/src/main/kotlin/com/dclass/backend/application/UUIDBasedPasswordGenerator.kt b/backend/src/main/kotlin/com/dclass/backend/application/UUIDBasedPasswordGenerator.kt new file mode 100644 index 0000000000..e9d3d26a4c --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/UUIDBasedPasswordGenerator.kt @@ -0,0 +1,11 @@ +package com.dclass.backend.application + +import org.springframework.stereotype.Component +import java.util.* + +@Component +class UUIDBasedPasswordGenerator: PasswordGenerator { + override fun generate(): String { + return UUID.randomUUID().toString().take(6) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/UserAuthenticationService.kt b/backend/src/main/kotlin/com/dclass/backend/application/UserAuthenticationService.kt new file mode 100644 index 0000000000..2f5d8bbe7d --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/UserAuthenticationService.kt @@ -0,0 +1,114 @@ +package com.dclass.backend.application + +import com.dclass.backend.application.dto.AuthenticateUserRequest +import com.dclass.backend.application.dto.LoginUserResponse +import com.dclass.backend.application.dto.RegisterUserRequest +import com.dclass.backend.domain.authenticationcode.AuthenticationCode +import com.dclass.backend.domain.authenticationcode.AuthenticationCodeRepository +import com.dclass.backend.domain.authenticationcode.getLastByEmail +import com.dclass.backend.domain.belong.Belong +import com.dclass.backend.domain.belong.BelongRepository +import com.dclass.backend.domain.blocklist.BlocklistRepository +import com.dclass.backend.domain.department.DepartmentRepository +import com.dclass.backend.domain.department.getByTitleOrThrow +import com.dclass.backend.domain.user.UniversityRepository +import com.dclass.backend.domain.user.UserRepository +import com.dclass.backend.domain.user.findByEmail +import com.dclass.backend.domain.user.getByEmailOrThrow +import com.dclass.backend.exception.university.UniversityException +import com.dclass.backend.exception.university.UniversityExceptionType.NOT_FOUND_UNIVERSITY +import com.dclass.backend.exception.user.UserException +import com.dclass.backend.exception.user.UserExceptionType +import com.dclass.backend.exception.user.UserExceptionType.RESIGNED_USER +import com.dclass.backend.security.JwtTokenProvider +import jakarta.persistence.EntityManager +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Transactional +@Service +class UserAuthenticationService( + private val userRepository: UserRepository, + private val authenticationCodeRepository: AuthenticationCodeRepository, + private val universityRepository: UniversityRepository, + private val belongRepository: BelongRepository, + private val departmentRepository: DepartmentRepository, + private val blocklistRepository: BlocklistRepository, + private val jwtTokenProvider: JwtTokenProvider, + private val entityManager: EntityManager +) { + fun generateTokenByRegister(request: RegisterUserRequest): LoginUserResponse { + require(request.password == request.confirmPassword) { "비밀번호가 일치하지 않습니다." } + + userRepository.findByEmail(request.email)?.also { + if(it.isDeleted()){ + blocklistRepository.findFirstByUserIdOrderByCreatedDateTimeDesc(it.id)?.validate() + } + if(!it.isDeleted()){ + throw UserException(UserExceptionType.ALREADY_EXIST_USER) + } + it.anonymizeEmail() + } + + entityManager.flush() + + authenticationCodeRepository.getLastByEmail(request.email) + .validate(request.authenticationCode) + + val univ = universityRepository.findByEmailSuffix(getEmailSuffix(request.email)) + val user = userRepository.save(request.toEntity(univ)) + + val majorDepartment = departmentRepository.getByTitleOrThrow(request.major) + val minorDepartment = departmentRepository.getByTitleOrThrow(request.minor) + + belongRepository.save( + Belong( + user.id, + listOf(majorDepartment.id, minorDepartment.id), + ) + ) + + return LoginUserResponse( + jwtTokenProvider.createAccessToken(user.email), + jwtTokenProvider.createRefreshToken(user.email) + ) + } + + fun generateTokenByLogin(request: AuthenticateUserRequest): LoginUserResponse { + val user = userRepository.getByEmailOrThrow(request.email) + if (user.isDeleted()) throw UserException(RESIGNED_USER) + + user.authenticate(request.password) + + return LoginUserResponse( + jwtTokenProvider.createAccessToken(user.email), + jwtTokenProvider.createRefreshToken(user.email) + ) + } + + fun generateAuthenticationCode(email: String): String { + userRepository.findByEmail(email)?.let { + if(it.isDeleted()){ + blocklistRepository.findFirstByUserIdOrderByCreatedDateTimeDesc(it.id)?.validate() + } + if(!it.isDeleted()){ + throw UserException(UserExceptionType.ALREADY_EXIST_USER) + } + } + if (!universityRepository.existsByEmailSuffix(getEmailSuffix(email))) { + throw UniversityException(NOT_FOUND_UNIVERSITY) + } + + val authenticationCode = authenticationCodeRepository.save(AuthenticationCode(email)) + return authenticationCode.code + } + + fun authenticateEmail(email: String, code: String) { + val authenticationCode = authenticationCodeRepository.getLastByEmail(email) + authenticationCode.authenticate(code) + } + + private fun getEmailSuffix(email: String): String { + return email.substringAfterLast("@") + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/UserBlockService.kt b/backend/src/main/kotlin/com/dclass/backend/application/UserBlockService.kt new file mode 100644 index 0000000000..2eae705f1a --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/UserBlockService.kt @@ -0,0 +1,16 @@ +package com.dclass.backend.application + +import com.dclass.backend.domain.userblock.UserBlock +import com.dclass.backend.domain.userblock.UserBlockRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Transactional +@Service +class UserBlockService( + private val userBlockRepository: UserBlockRepository +) { + fun block(userId: Long, blockedUserId: Long) { + userBlockRepository.save(UserBlock(userId, blockedUserId)) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/UserService.kt b/backend/src/main/kotlin/com/dclass/backend/application/UserService.kt new file mode 100644 index 0000000000..8508c178e7 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/UserService.kt @@ -0,0 +1,71 @@ +package com.dclass.backend.application + +import com.dclass.backend.application.dto.EditPasswordRequest +import com.dclass.backend.application.dto.ResetPasswordRequest +import com.dclass.backend.application.dto.UpdateNicknameRequest +import com.dclass.backend.application.dto.UserResponseWithDepartmentNames +import com.dclass.backend.domain.belong.BelongRepository +import com.dclass.backend.domain.belong.getOrThrow +import com.dclass.backend.domain.department.DepartmentRepository +import com.dclass.backend.domain.user.UserRepository +import com.dclass.backend.domain.user.getByEmailOrThrow +import com.dclass.backend.domain.user.getOrThrow +import com.dclass.backend.exception.department.DepartmentException +import com.dclass.backend.exception.department.DepartmentExceptionType.NOT_FOUND_DEPARTMENT +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + + +@Transactional +@Service +class UserService( + private val userRepository: UserRepository, + private val passwordGenerator: PasswordGenerator, + private val departmentRepository: DepartmentRepository, + private val belongRepository: BelongRepository, +) { + fun resetPassword(request: ResetPasswordRequest) { + val user = userRepository.getByEmailOrThrow(request.email) + user.resetPassword(request.name, passwordGenerator.generate()) + userRepository.save(user) + } + + fun editPassword(id: Long, request: EditPasswordRequest) { + require(request.password == request.confirmPassword) { "새 비밀번호가 일치하지 않습니다." } + + val user = userRepository.getOrThrow(id) + user.changePassword(request.oldPassword, request.password) + } + + fun editNickname(id: Long, request: UpdateNicknameRequest) { + val user = userRepository.getOrThrow(id) + + user.changeNickname(request.nickname) + } + + fun getInformation(id: Long): UserResponseWithDepartmentNames { + val user = userRepository.getOrThrow(id) + + val belong = belongRepository.getOrThrow(id) + + val departments = departmentRepository.findAllById(belong.departmentIds) + + val groupBy = belong.departmentIds.associateWith { departmentId -> + departments.find { it.id == departmentId } + ?: throw DepartmentException(NOT_FOUND_DEPARTMENT) + } + + + return UserResponseWithDepartmentNames( + user, + groupBy[belong.major]!!.title, + groupBy[belong.minor]!!.title, + groupBy[belong.activated]!!.title + ) + } + + fun resign(id: Long) { + val user = userRepository.getOrThrow(id) + user.anonymize() + } +} diff --git a/backend/src/main/kotlin/com/dclass/backend/application/dto/BelongDtos.kt b/backend/src/main/kotlin/com/dclass/backend/application/dto/BelongDtos.kt new file mode 100644 index 0000000000..c072ef11b7 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/dto/BelongDtos.kt @@ -0,0 +1,32 @@ +package com.dclass.backend.application.dto + +import io.swagger.v3.oas.annotations.media.Schema +import java.time.Duration + +data class SwitchDepartmentResponse( + @Schema(description = "활성화된 학과 이름", example = "컴퓨터공학과") + val activated: String, +) + +data class UpdateDepartmentRequest( + @Schema(description = "변경할 전공의 이름", example = "컴퓨터공학과") + val major: String, + + @Schema(description = "변경할 부전공의 이름", example = "경영학과") + val minor: String, +) + +data class RemainDurationResponse( + @Schema(description = "남은 일수", example = "10") + val remainDays: Long, + @Schema(description = "남은 시간", example = "10") + val remainHours: Long, + @Schema(description = "남은 분", example = "10") + val remainMinutes: Long, +) { + constructor(remain: Duration) : this( + remain.toDays(), + remain.toHours(), + remain.toMinutes(), + ) +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/dto/CommentDtos.kt b/backend/src/main/kotlin/com/dclass/backend/application/dto/CommentDtos.kt new file mode 100644 index 0000000000..ca0ae0b8b1 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/dto/CommentDtos.kt @@ -0,0 +1,342 @@ +package com.dclass.backend.application.dto + +import com.dclass.backend.domain.comment.Comment +import com.dclass.backend.domain.comment.CommentLikes +import com.dclass.backend.domain.user.User +import com.dclass.backend.domain.user.UserInformation +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotNull +import java.time.LocalDateTime + +data class CommentRequest( + @Schema( + description = "댓글의 내용", + example = "댓글 내용" + ) + @field:NotNull + val content: String, +) + +data class CreateCommentRequest( + @Schema( + description = "게시글의 고유 식별자", + example = "1" + ) + val postId: Long, + + @Schema( + description = "댓글의 내용", + example = "댓글 내용" + ) + @field:NotNull + val content: String, +) { + fun toEntity(userId: Long): Comment { + return Comment(userId, postId, content) + } +} + +data class UpdateCommentRequest( + @Schema( + description = "댓글의 고유 식별자", + example = "1" + ) + val commentId: Long, + + @Schema( + description = "댓글의 내용", + example = "댓글 내용" + ) + @field:NotNull + val content: String, +) + +data class DeleteCommentRequest( + @Schema( + description = "댓글의 고유 식별자", + example = "1" + ) + val commentId: Long, +) + +data class LikeCommentRequest( + @Schema( + description = "댓글의 고유 식별자", + example = "1" + ) + val commentId: Long, +) + + +data class CommentResponse( + @Schema( + description = "댓글의 고유 식별자", + example = "1" + ) + val id: Long, + + @Schema( + description = "댓글을 작성한 유저의 고유 식별자", + example = "1" + ) + val userId: Long, + + @Schema( + description = "댓글이 달린 게시글의 고유 식별자", + example = "1" + ) + val postId: Long, + + @Schema( + description = "댓글의 내용", + example = "댓글 내용" + ) + val content: String, + + @Schema( + description = "댓글이 작성된 시각", + example = "2021-08-01T00:00:00" + ) + val createdDateTime: LocalDateTime, + + @Schema( + description = "댓글이 수정된 시각", + example = "2021-08-01T00:00:00" + ) + val modifiedDateTime: LocalDateTime, +) { + constructor(comment: Comment) : this( + comment.id, + comment.userId, + comment.postId, + comment.content, + comment.createdDateTime, + comment.modifiedDateTime + ) +} + + +data class CommentWithUserResponse( + @Schema( + description = "댓글의 고유 식별자", + example = "1" + ) + val id: Long, + + @Schema( + description = "댓글을 작성한 유저의 정보", + example = "1" + ) + val userInformation: UserInformation, + + @Schema( + description = "댓글을 작성한 유저의 고유 id", + example = "1" + ) + val userId : Long, + + @Schema( + description = "댓글이 달린 게시글의 고유 식별자", + example = "1" + ) + val postId: Long, + + @Schema( + description = "댓글의 내용", + example = "댓글 내용" + ) + val content: String, + + @Schema( + description = "댓글의 좋아요 수", + example = "1" + ) + val likeCount: CommentLikes, + + @Schema( + description = "댓글의 삭제 여부", + example = "false" + ) + val deleted: Boolean, + + @Schema( + description = "댓글의 좋아요 여부", + example = "true" + ) + var isLiked: Boolean, + + @Schema( + description = "차단된 사용자 여부", + example = "true" + ) + var isBlockedUser: Boolean, + + @Schema( + description = "댓글이 작성된 시각", + example = "2021-08-01T00:00:00" + ) + val createdAt: LocalDateTime, +) { + constructor(comment: Comment, user: User) : this( + id = comment.id, + userInformation = UserInformation(user.name, user.email, user.nickname), + userId = user.id, + postId = comment.postId, + content = comment.content.takeIf { !comment.isDeleted() } ?: "삭제된 댓글 입니다.", + likeCount = comment.commentLikes, + deleted = comment.isDeleted(), + isLiked = false, + isBlockedUser = false, + createdAt = comment.createdDateTime + ) +} + +data class CommentReplyWithUserResponse( + @Schema( + description = "댓글의 고유 식별자", + example = "1" + ) + val id: Long, + + @Schema( + description = "댓글을 작성한 유저의 정보", + example = "1" + ) + val userInformation: UserInformation, + + @Schema( + description = "댓글을 작성한 유저의 고유 id", + example = "1" + ) + val userId : Long, + + @Schema( + description = "댓글이 달린 게시글의 고유 식별자", + example = "1" + ) + val postId: Long, + + @Schema( + description = "댓글의 내용", + example = "댓글 내용" + ) + val content: String, + + @Schema( + description = "댓글의 좋아요 수", + example = """ + { + "likes": [1, 2], + "count": 2 + } + """ + ) + val likeCount: CommentLikes, + + @Schema( + description = "댓글의 삭제 여부", + example = "false" + ) + val deleted: Boolean, + + @Schema( + description = "댓글의 좋아요 여부", + example = "true" + ) + val isLiked: Boolean, + + @Schema( + description = "차단된 사용자 여부", + example = "true" + ) + var isBlockedUser: Boolean, + + @Schema( + description = "댓글이 작성된 시각", + example = "2021-08-01T00:00:00" + ) + val createdAt: LocalDateTime, + + @Schema( + description = "댓글의 답글 목록", + example = """ + [ + { + "id": 1, + "userId": 1, + "userInformation": { + "name": "이름", + "email": "이메일", + "nickname": "닉네임" + }, + "commentId": 1, + "content": "대댓글 내용", + "likeCount": { + "likes": [1, 2], + "count": 2 + }, + "createdAt": "2021-08-01T00:00:00" + } + ] + """ + ) + val replies: List + +) { + constructor( + commentWithUserResponse: CommentWithUserResponse, + replies: List + ) : this( + id = commentWithUserResponse.id, + userInformation = commentWithUserResponse.userInformation, + userId = commentWithUserResponse.userId, + postId = commentWithUserResponse.postId, + content = commentWithUserResponse.content, + likeCount = commentWithUserResponse.likeCount, + deleted = commentWithUserResponse.deleted, + isLiked = commentWithUserResponse.isLiked, + createdAt = commentWithUserResponse.createdAt, + isBlockedUser = commentWithUserResponse.isBlockedUser, + replies = replies + ) +} + +data class CommentScrollPageRequest( + @Schema( + description = "게시글의 고유 식별자", + example = "1" + ) + val postId: Long, + + @Schema( + description = "댓글의 마지막 고유 식별자", + example = "1" + ) + val lastCommentId: Long? = null, + + @Schema( + description = "댓글의 페이지 크기", + example = "20" + ) + val size: Int = 20, +) + +data class CommentsResponse( + @Schema( + description = "댓글 목록", + example = "['댓글 목록']" + ) + val data: List, + + @Schema( + description = "댓글 목록의 메타데이터", + example = "{'count': 10, 'hasMore': true}" + ) + val meta: MetaData +) { + companion object { + fun of(data: List, limit: Int): CommentsResponse { + return CommentsResponse(data, MetaData(data.size, data.size >= limit)) + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/dto/NotificationDtos.kt b/backend/src/main/kotlin/com/dclass/backend/application/dto/NotificationDtos.kt new file mode 100644 index 0000000000..acdb7e9a77 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/dto/NotificationDtos.kt @@ -0,0 +1,189 @@ +package com.dclass.backend.application.dto + +import com.dclass.backend.domain.notification.Notification +import com.dclass.backend.domain.notification.NotificationType +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDateTime + +abstract class NotificationRequest { + abstract val userId: Long + abstract fun toEntity(): Notification + abstract fun createResponse(id: Long, createdAt: LocalDateTime): NotificationResponse +} + +data class NotificationCommentRequest( + override val userId: Long, + val postId: Long, + val commentId: Long, + val content: String, + val communityTitle: String, + val type: NotificationType +) : NotificationRequest() { + override fun toEntity(): Notification { + return Notification(userId, postId, content, type = type) + } + + override fun createResponse(id: Long, createdAt: LocalDateTime): NotificationResponse { + return NotificationCommentResponse( + id = id, + userId = userId, + postId = postId, + commentId = commentId, + content = content, + type = type, + createdAt = createdAt, + isRead = false, + communityTitle = communityTitle + ) + } +} + +data class NotificationReplyRequest( + override val userId: Long, + val postId: Long, + val commentId: Long, + val replyId: Long, + val content: String, + val communityTitle: String, + val type: NotificationType +) : NotificationRequest() { + override fun toEntity(): Notification { + return Notification(userId, postId, content, type = type) + } + + override fun createResponse(id: Long, createdAt: LocalDateTime): NotificationResponse { + return NotificationReplyResponse( + id = id, + userId = userId, + postId = postId, + commentId = commentId, + replyId = replyId, + content = content, + type = type, + createdAt = createdAt, + isRead = false, + communityTitle = communityTitle + ) + } +} + + +abstract class NotificationResponse + +data class NotificationCommentResponse( + @Schema( + description = "알림의 고유 식별자", + example = "1" + ) + val id: Long, + + @Schema( + description = "알림을 받은 유저의 고유 식별자", + example = "1" + ) + val userId: Long, + + @Schema( + description = "알림이 발생한 게시글의 고유 식별자", + example = "1" + ) + val postId: Long, + + @Schema( + description = "알림이 발생한 댓글의 고유 식별자", + example = "1" + ) + val commentId: Long, + + @Schema( + description = "알림의 내용", + example = "댓글 내용" + ) + val content: String, + + @Schema( + description = "알림의 타입", + example = "COMMENT" + ) + val type: NotificationType, + + @Schema( + description = "알림이 생성된 시각", + example = "2021-08-01T00:00:00" + ) + val createdAt: LocalDateTime, + + @Schema( + description = "알림을 읽었는지 여부", + example = "false" + ) + val isRead: Boolean, + + @Schema( + description = "알림이 발생한 커뮤니티의 타이틀", + example = "자유게시판" + ) + val communityTitle: String, +) : NotificationResponse() + +data class NotificationReplyResponse( + @Schema( + description = "알림의 고유 식별자", + example = "1" + ) + val id: Long, + + @Schema( + description = "알림을 받은 유저의 고유 식별자", + example = "1" + ) + val userId: Long, + + @Schema( + description = "알림이 발생한 게시글의 고유 식별자", + example = "1" + ) + val postId: Long, + + @Schema( + description = "알림이 발생한 댓글의 고유 식별자", + example = "1" + ) + val commentId: Long, + + @Schema( + description = "알림이 발생한 답글의 고유 식별자", + example = "1" + ) + val replyId: Long, + + @Schema( + description = "알림의 내용", + example = "답글 내용" + ) + val content: String, + + @Schema( + description = "알림의 타입", + example = "REPLY" + ) + val type: NotificationType, + + @Schema( + description = "알림이 생성된 시각", + example = "2021-08-01T00:00:00" + ) + val createdAt: LocalDateTime, + + @Schema( + description = "알림을 읽었는지 여부", + example = "false" + ) + val isRead: Boolean, + + @Schema( + description = "알림이 발생한 커뮤니티의 타이틀", + example = "자유게시판" + ) + val communityTitle: String, +) : NotificationResponse() \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/dto/PostDtos.kt b/backend/src/main/kotlin/com/dclass/backend/application/dto/PostDtos.kt new file mode 100644 index 0000000000..9de3f9dea8 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/dto/PostDtos.kt @@ -0,0 +1,399 @@ +package com.dclass.backend.application.dto + +import com.dclass.backend.domain.community.CommunityType +import com.dclass.backend.domain.post.Post +import com.dclass.backend.domain.post.PostCount +import com.dclass.backend.domain.user.User +import com.dclass.support.domain.Image +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDateTime + + +data class PostScrollPageRequest( + @Schema( + description = "게시글의 마지막 식별자", + example = "1" + ) + val lastId: Long? = null, + + @Schema( + description = "커뮤니티 타이틀", + example = "자유게시판" + ) + var communityTitle: String? = null, + + @Schema( + description = "게시글의 개수", + example = "10" + ) + val size: Int, + + @Schema( + description = "게시글의 인기순 정렬 여부", + example = "true" + ) + val isHot: Boolean = false, + + @Schema( + description = "검색 키워드", + example = "검색 키워드" + ) + val keyword: String? = null, +) { + init { + communityTitle = CommunityType.from(communityTitle)?.name + } +} + +/** + * modifiedDateTime은 필요할까? + */ +data class PostResponse( + @Schema( + description = "게시글의 고유 식별자", + example = "1" + ) + val id: Long, + + @Schema( + description = "게시글을 작성한 사용자의 고유 식별자", + example = "1" + ) + val userId: Long, + + @Schema( + description = "게시글을 작성한 사용자의 닉네임", + example = "닉네임" + ) + val userNickname: String, + + @Schema( + description = "게시글을 작성한 사용자의 대학교 이름", + example = "대학교 이름" + ) + val universityName: String, + + @Schema( + description = "게시글이 속한 커뮤니티의 고유 식별자", + example = "1" + ) + val communityId: Long, + + @Schema( + description = "게시글이 속한 커뮤니티의 타이틀", + example = "자유게시판" + ) + var communityTitle: String, + + @Schema( + description = "게시글의 제목", + example = "게시글 제목" + ) + val postTitle: String, + + @Schema( + description = "게시글의 내용", + example = "게시글 내용" + ) + val postContent: String, + + @Schema( + description = "게시글의 이미지 URL 리스트", + example = "['이미지 URL']" + ) + var images: List, + + @Schema( + description = "게시글의 조회수, 댓글수, 좋아요수", + example = "{'view': 1, 'comment': 1, 'like': 1}" + ) + val count: PostCount, + + @Schema( + description = "게시글이 질문인지 여부", + example = "true" + ) + val isQuestion: Boolean, + + @Schema( + description = "차단한 사용자의 게시글인지 여부", + example = "true" + ) + var isBlockedUser: Boolean = false, + + @Schema( + description = "게시글의 이미지 개수", + example = "1" + ) + val imageCount: Int, + + @Schema( + description = "게시글이 작성된 시각", + example = "2021-08-01T00:00:00" + ) + val createdDateTime: LocalDateTime, +) { + constructor( + post: Post, + user: User, + communityTitle: String, + ) : this( + post.id, + post.userId, + user.nickname, + user.universityName, + post.communityId, + communityTitle, + post.title, + post.content, + post.images.map { it.imageKey }, + post.postCount, + post.isQuestion, + false, + post.images.size, + post.createdDateTime + ) + +} + +data class PostDetailResponse( + @Schema( + description = "게시글의 고유 식별자", + example = "1" + ) + val id: Long, + + @Schema( + description = "게시글을 작성한 사용자의 고유 식별자", + example = "1" + ) + val userId: Long, + + @Schema( + description = "게시글을 작성한 사용자의 닉네임", + example = "닉네임" + ) + val userNickname: String, + + @Schema( + description = "게시글을 작성한 사용자의 대학교 이름", + example = "대학교 이름" + ) + val universityName: String, + + @Schema( + description = "게시글이 속한 커뮤니티의 고유 식별자", + example = "1" + ) + val communityId: Long, + + @Schema( + description = "게시글이 속한 커뮤니티의 타이틀", + example = "자유게시판" + ) + var communityTitle: String, + + @Schema( + description = "게시글의 제목", + example = "게시글 제목" + ) + val postTitle: String, + + @Schema( + description = "게시글의 내용", + example = "게시글 내용" + ) + val postContent: String, + + @Schema( + description = "게시글의 이미지 URL 리스트", + example = "['이미지 URL']" + ) + var images: List, + + @Schema( + description = "게시글의 조회수, 댓글수, 좋아요수", + example = "{'view': 1, 'comment': 1, 'like': 1}" + ) + val count: PostCount, + + @Schema( + description = "게시글이 질문인지 여부", + example = "true" + ) + val isQuestion: Boolean, + + @Schema( + description = "게시글이 스크랩된 여부", + example = "true" + ) + var isScrapped: Boolean, + + @Schema( + description = "게시글의 좋아요 여부", + example = "true" + ) + var likedBy: Boolean, + + @Schema( + description = "게시글의 이미지 개수", + example = "1" + ) + val imageCount: Int, + + @Schema( + description = "차단한 사용자의 게시글인지 여부", + example = "true" + ) + var isBlockedUser: Boolean = false, + + @Schema( + description = "게시글이 작성된 시각", + example = "2021-08-01T00:00:00" + ) + val createdDateTime: LocalDateTime, +) { + + constructor( + post: Post, + user: User, + communityTitle: String, + ) : this( + post.id, + post.userId, + user.nickname, + user.universityName, + post.communityId, + communityTitle, + post.title, + post.content, + post.images.map { it.imageKey }, + post.postCount, + post.isQuestion, + false, + false, + post.images.size, + false, + post.createdDateTime + ) +} + +data class CreatePostRequest( + @Schema( + description = "게시글이 속한 커뮤니티의 타이틀", + example = "자유게시판" + ) + var communityTitle: String, + + @Schema( + description = "게시글의 제목", + example = "게시글 제목" + ) + val title: String, + + @Schema( + description = "게시글의 내용", + example = "게시글 내용" + ) + val content: String, + + @Schema( + description = "게시글이 질문인지 여부", + example = "true" + ) + val isQuestion: Boolean, + + @Schema( + description = "게시글의 이미지 URL 리스트", + example = "['이미지 URL']" + ) + val images: List, +) { + + fun toEntity(userId: Long, communityId: Long) = Post( + userId = userId, + communityId = communityId, + title = title, + content = content, + images = images.map { Image(it) }, + isQuestion = isQuestion, + ) + + init { + communityTitle = CommunityType.from(communityTitle)!!.name + } +} + +data class UpdatePostRequest( + @Schema( + description = "게시글의 고유 식별자", + example = "1" + ) + val postId: Long, + + @Schema( + description = "게시글의 제목", + example = "게시글 제목" + ) + val title: String, + + @Schema( + description = "게시글의 내용", + example = "게시글 내용" + ) + val content: String, + + @Schema( + description = "게시글이 속한 커뮤니티의 타이틀", + example = "자유게시판" + ) + var communityTitle: String, + + @Schema( + description = "게시글의 이미지 URL 리스트", + example = "['이미지 URL']" + ) + var images: List, +) + +data class DeletePostRequest( + @Schema( + description = "게시글의 고유 식별자", + example = "1" + ) + val postId: Long +) + +data class MetaData( + @Schema( + description = "현재 페이지의 게시글 개수", + example = "10" + ) + val count: Int, + + @Schema( + description = "다음 페이지가 있는지 여부", + example = "true" + ) + val hasMore: Boolean +) + +data class PostsResponse( + @Schema( + description = "게시글 목록", + example = "['게시글 목록']" + ) + val data: List, + + @Schema( + description = "게시글 목록의 메타데이터", + example = "{'count': 10, 'hasMore': true}" + ) + val meta: MetaData +) { + companion object { + fun of(data: List, limit: Int): PostsResponse { + return PostsResponse(data, MetaData(data.size, data.size >= limit)) + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/dto/ReplyDtos.kt b/backend/src/main/kotlin/com/dclass/backend/application/dto/ReplyDtos.kt new file mode 100644 index 0000000000..5d46c267dd --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/dto/ReplyDtos.kt @@ -0,0 +1,183 @@ +package com.dclass.backend.application.dto + +import com.dclass.backend.domain.reply.Reply +import com.dclass.backend.domain.reply.ReplyLikes +import com.dclass.backend.domain.user.User +import com.dclass.backend.domain.user.UserInformation +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotNull +import java.time.LocalDateTime + +data class ReplyRequest( + + @Schema( + description = "대댓글의 내용", + example = "대댓글 내용" + ) + val content: String, +) + +data class CreateReplyRequest( + + @Schema( + description = "댓글의 고유 식별자", + example = "1" + ) + @field:NotNull + val commentId: Long, + + @Schema( + description = "대댓글의 내용", + example = "대댓글 내용" + ) + @field:NotNull + val content: String, +) { + fun toEntity(userId: Long): Reply { + return Reply(userId, commentId, content) + } +} + +data class UpdateReplyRequest( + @Schema( + description = "대댓글의 고유 식별자", + example = "1" + ) + val replyId: Long, + + @Schema( + description = "대댓글의 내용", + example = "대댓글 내용" + ) + @field:NotNull + val content: String, +) + +data class DeleteReplyRequest( + @Schema( + description = "대댓글의 고유 식별자", + example = "1" + ) + val replyId: Long, +) + +data class LikeReplyRequest( + @Schema( + description = "대댓글의 고유 식별자", + example = "1" + ) + val replyId: Long, +) + +data class ReplyResponse( + @Schema( + description = "댓글의 고유 식별자", + example = "1" + ) + val id: Long, + + @Schema( + description = "댓글을 작성한 유저의 고유 식별자", + example = "1" + ) + val userId: Long, + + @Schema( + description = "대댓글이 달린 댓글의 고유 식별자", + example = "1" + ) + val commentId: Long, + + @Schema( + description = "대댓글의 내용", + example = "대댓글 내용" + ) + val content: String, + + @Schema( + description = "대댓글의 좋아요 수", + example = "0" + ) + val likeCount: ReplyLikes, + + @Schema( + description = "대댓글이 작성된 시각", + example = "2021-08-01T00:00:00" + ) + val createdAt: LocalDateTime, +) { + constructor(reply: Reply) : this( + id = reply.id, + userId = reply.userId, + commentId = reply.commentId, + content = reply.content, + likeCount = reply.replyLikes, + createdAt = reply.createdDateTime + ) +} + +data class ReplyWithUserResponse( + @Schema( + description = "대댓글의 고유 식별자", + example = "1" + ) + val id: Long, + + @Schema( + description = "대댓글을 작성한 유저의 고유 식별자", + example = "1" + ) + val userId: Long, + + @Schema( + description = "대댓글을 작성한 유저의 정보", + ) + val userInformation: UserInformation, + + @Schema( + description = "대댓글이 달린 댓글의 고유 식별자", + example = "1" + ) + val commentId: Long, + + @Schema( + description = "대댓글의 내용", + example = "대댓글 내용" + ) + val content: String, + + @Schema( + description = "대댓글의 좋아요 수", + example = "0" + ) + val likeCount: ReplyLikes, + + @Schema( + description = "차단한 사용자 여부", + example = "false" + ) + var isBlockedUser: Boolean = false, + + @Schema( + description = "대댓글이 작성된 시각", + example = "2021-08-01T00:00:00" + ) + val createdAt: LocalDateTime, +) { + constructor(reply: Reply, user: User) : this( + id = reply.id, + userId = reply.userId, + userInformation = UserInformation(user.name, user.email, user.nickname), + commentId = reply.commentId, + content = reply.content, + likeCount = reply.replyLikes, + createdAt = reply.createdDateTime + ) +} + +data class ReplyValidatorDto( + val postId: Long, + val postUserId: Long, + val commentUserId: Long, + val communityTitle: String, +) \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/dto/ReportDto.kt b/backend/src/main/kotlin/com/dclass/backend/application/dto/ReportDto.kt new file mode 100644 index 0000000000..882dd0768f --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/dto/ReportDto.kt @@ -0,0 +1,13 @@ +package com.dclass.backend.application.dto + +import com.dclass.backend.domain.report.Report +import com.dclass.backend.domain.report.ReportReason +import com.dclass.backend.domain.report.ReportType + +data class UpdateReportRequest( + val reportedObjectId: Long, + val reportType: ReportType, + val reason: ReportReason +){ + fun toEntity(reporterId: Long) = Report(reporterId, reportedObjectId, reportType, reason) +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/dto/UserDtos.kt b/backend/src/main/kotlin/com/dclass/backend/application/dto/UserDtos.kt new file mode 100644 index 0000000000..9627b5179a --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/dto/UserDtos.kt @@ -0,0 +1,243 @@ +package com.dclass.backend.application.dto + +import com.dclass.backend.domain.belong.Belong +import com.dclass.backend.domain.user.Password +import com.dclass.backend.domain.user.University +import com.dclass.backend.domain.user.User +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.Pattern + +data class UserResponse( + @Schema( + description = "유저의 고유 식별자", + example = "1" + ) + val id: Long, + + @Schema( + description = "유저의 이름", + example = "쿠민이" + ) + val name: String, + + @Schema( + description = "유저의 대학교 이메일", + example = "test@kookmin.ac.kr" + ) + val email: String, + + @Schema( + description = "유저의 닉네임", + example = "ku-mini" + ) + val nickname: String, + + @Schema( + description = "유저의 대학교 이름", + example = "국민대학교" + ) + val universityName: String, +) { + constructor(user: User) : this( + user.id, + user.name, + user.email, + user.nickname, + user.universityName + ) +} + + +data class UserResponseWithDepartment( + val userResponse: UserResponse, + + @Schema( + description = "유저의 소속 학과들의 고유 식별자 리스트", + example = "[1, 2]" + ) + val departmentIds: List, +) { + constructor(user: User, belong: Belong) : this( + UserResponse(user), + belong.departmentIds + ) +} + + +data class UserResponseWithDepartmentNames( + @Schema( + description = "유저의 고유 식별자", + example = "1" + ) + val id: Long, + + @Schema( + description = "유저의 이름", + example = "쿠민이" + ) + val name: String, + + @Schema( + description = "유저의 대학교 이메일", + example = "test@kookmin.ac.kr" + ) + val email: String, + + @Schema( + description = "유저의 닉네임", + example = "ku-mini" + ) + val nickname: String, + + @Schema( + description = "유저의 대학교 이름", + example = "국민대학교" + ) + val universityName: String, + @Schema( + description = "유저의 전공 학과 이름", + example = "컴퓨터공학과" + ) + val major: String, + + @Schema( + description = "유저의 부전공 학과 이름", + example = "소프트웨어학과" + ) + val minor: String = "", + + val activatedDepartment: String, +) { + constructor(user: User, major: String, minor: String, activatedDepartment: String) : this( + user.id, + user.name, + user.email, + user.nickname, + user.universityName, + major = major, + minor = minor, + activatedDepartment = activatedDepartment + ) +} + +data class RegisterUserRequest( + @Schema( + description = "유저의 이름", + example = "쿠민이" + ) + @field:Pattern(regexp = "[가-힣]{1,30}", message = "올바른 형식의 이름이어야 합니다") + val name: String, + + @Schema( + description = "유저의 대학교 이메일", + example = "dasdsa@kookmin.ac.kr" + ) + @field:Email + val email: String, + + @Schema( + description = "유저의 닉네임", + example = "ku-mini" + ) + val nickname: String, + + @Schema( + description = "유저의 비밀번호", + example = "password" + ) + val password: String, + + @Schema( + description = "유저의 비밀번호 확인", + example = "password" + ) + val confirmPassword: String, + + @Schema( + description = "유저의 대학교 인증 코드", + example = "123456" + ) + val authenticationCode: String, + + @Schema( + description = "유저의 전공 학과 이름", + example = "컴퓨터공학과" + ) + val major: String, + + @Schema( + description = "유저의 부전공 학과 이름", + example = "경영학과" + ) + val minor: String, +) { + fun toEntity(univ: University): User { + return User(name, email, nickname, password, univ) + } +} + +data class AuthenticateUserRequest( + @Schema( + description = "유저의 대학교 이메일", + example = "dsadsadfqw@kookmin.ac.kr" + ) + @field:Email + val email: String, + + @Schema( + description = "유저의 대학교 인증 코드", + example = "123456" + ) + val password: Password +) + +data class ResetPasswordRequest( + @Schema( + description = "유저의 이름", + example = "쿠민이" + ) + @field:Pattern(regexp = "[가-힣]{1,30}", message = "올바른 형식의 이름이어야 합니다") + val name: String, + + @Schema( + description = "유저의 대학교 이메일", + example = "daaedwqda@kookmin.ac.kr" + ) + @field:Email + val email: String, +) + +data class EditPasswordRequest( + @Schema( + description = "유저의 기존 비밀번호", + example = "password" + ) + val oldPassword: Password, + + @Schema( + description = "유저의 새로운 비밀번호", + example = "password" + ) + val password: Password, + + @Schema( + description = "유저의 새로운 비밀번호 확인", + example = "password" + ) + val confirmPassword: Password +) + +data class LoginUserResponse( + val accessToken: String, + val refreshToken: String +) + + +data class UpdateNicknameRequest( + @Schema( + description = "유저의 변경할 새로운 닉네임", + example = "ku-mini" + ) + val nickname: String +) \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/mail/MailSender.kt b/backend/src/main/kotlin/com/dclass/backend/application/mail/MailSender.kt new file mode 100644 index 0000000000..b10a92f96d --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/mail/MailSender.kt @@ -0,0 +1,5 @@ +package com.dclass.backend.application.mail + +interface MailSender { + suspend fun send(toAddress: String, subjectVal: String, bodyHtml: String) +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/application/mail/MailService.kt b/backend/src/main/kotlin/com/dclass/backend/application/mail/MailService.kt new file mode 100644 index 0000000000..10618f6d4b --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/application/mail/MailService.kt @@ -0,0 +1,48 @@ +package com.dclass.backend.application.mail + +import com.dclass.backend.domain.user.PasswordResetEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.springframework.stereotype.Service +import org.springframework.transaction.event.TransactionalEventListener +import org.thymeleaf.context.Context +import org.thymeleaf.spring6.ISpringTemplateEngine + +@Service +class MailService( + private val mailSender: MailSender, + private val templateEngine: ISpringTemplateEngine +) { + fun sendAuthenticationCodeMail(email: String, authenticationCode: String) = + CoroutineScope(Dispatchers.IO).launch { + val context = Context().apply { + setVariables(mapOf("authenticationCode" to authenticationCode)) + } + + mailSender.send( + email, + "메일 인증 코드를 발송해 드립니다. ", + templateEngine.process("mail/email-authentication.html", context) + ) + } + + @TransactionalEventListener + fun sendPasswordResetMail(event: PasswordResetEvent) = + CoroutineScope(Dispatchers.IO).launch { + val context = Context().apply { + setVariables( + mapOf( + "name" to event.name, + "password" to event.password + ) + ) + } + + mailSender.send( + event.email, + "${event.name}님, 임시 비밀번호를 발송해 드립니다.", + templateEngine.process("mail/password-reset.html", context) + ) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/common/QueryAop.kt b/backend/src/main/kotlin/com/dclass/backend/common/QueryAop.kt new file mode 100644 index 0000000000..ad511c8177 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/common/QueryAop.kt @@ -0,0 +1,23 @@ +package com.dclass.backend.common + +import com.dclass.support.util.logger +import org.aspectj.lang.annotation.After +import org.aspectj.lang.annotation.Aspect +import org.springframework.stereotype.Component +import org.springframework.web.context.request.RequestContextHolder +import org.springframework.web.context.request.ServletRequestAttributes + +@Aspect +@Component +class QueryAop { + private val log = logger() + + @After("within(@org.springframework.web.bind.annotation.RestController *)") + fun logQueryInfo() { + val attributes = RequestContextHolder.getRequestAttributes() as? ServletRequestAttributes + val request = attributes?.request + request?.let { + log.info("METHOD: ${request.method}, URI: ${request.requestURI}") + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/config/AuthenticationConfig.kt b/backend/src/main/kotlin/com/dclass/backend/config/AuthenticationConfig.kt new file mode 100644 index 0000000000..ba2b178ed4 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/config/AuthenticationConfig.kt @@ -0,0 +1,18 @@ +package com.dclass.backend.config + +import com.dclass.backend.security.AuthTokenResolver +import com.dclass.backend.security.LoginUserResolver +import org.springframework.context.annotation.Configuration +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +@Configuration +class AuthenticationConfig( + private val loginUserResolver: LoginUserResolver, + private val authTokenResolver: AuthTokenResolver +) : WebMvcConfigurer { + override fun addArgumentResolvers(resolvers: MutableList) { + resolvers.add(loginUserResolver) + resolvers.add(authTokenResolver) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/config/Database.kt b/backend/src/main/kotlin/com/dclass/backend/config/Database.kt new file mode 100644 index 0000000000..711ae1f560 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/config/Database.kt @@ -0,0 +1,43 @@ +package com.dclass.backend.config + +import jakarta.persistence.EntityManager +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Component + +interface Database{ + + fun retrieveTables(): List + fun clear(tableNames: List) +} + +abstract class AbstractDatabase( + private val entityManager: EntityManager +) : Database { + override fun clear(tableNames: List) { + entityManager.createNativeQuery(constraintsOffSql).executeUpdate() + tableNames + .forEach { entityManager.createNativeQuery(createTruncateTableSql(it)).executeUpdate() } + entityManager.createNativeQuery(constraintsOnSql).executeUpdate() + } + + override fun retrieveTables(): List { + return entityManager.createNativeQuery(metaTablesSql).resultList + .map { it.toString() } + } + + abstract val metaTablesSql: String + abstract val constraintsOffSql: String + abstract val constraintsOnSql: String + abstract fun createTruncateTableSql(tableName: String): String +} + +@Profile("local") +@Component +class MySql( + entityManager: EntityManager +): AbstractDatabase(entityManager) { + override val metaTablesSql: String = "show tables" + override val constraintsOffSql: String = "set foreign_key_checks = 0" + override val constraintsOnSql: String = "set foreign_key_checks = 1" + override fun createTruncateTableSql(tableName: String): String = "truncate table $tableName" +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/config/DatabaseInitializer.kt b/backend/src/main/kotlin/com/dclass/backend/config/DatabaseInitializer.kt new file mode 100644 index 0000000000..a5d7cef517 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/config/DatabaseInitializer.kt @@ -0,0 +1,850 @@ +package com.dclass.backend.config + +import com.dclass.backend.application.CommentService +import com.dclass.backend.application.PostService +import com.dclass.backend.application.ReplyService +import com.dclass.backend.domain.belong.Belong +import com.dclass.backend.domain.belong.BelongRepository +import com.dclass.backend.domain.comment.Comment +import com.dclass.backend.domain.comment.CommentRepository +import com.dclass.backend.domain.community.Community +import com.dclass.backend.domain.community.CommunityRepository +import com.dclass.backend.domain.department.Department +import com.dclass.backend.domain.department.DepartmentRepository +import com.dclass.backend.domain.post.Post +import com.dclass.backend.domain.post.PostLikes +import com.dclass.backend.domain.post.PostRepository +import com.dclass.backend.domain.reply.Reply +import com.dclass.backend.domain.reply.ReplyRepository +import com.dclass.backend.domain.user.University +import com.dclass.backend.domain.user.UniversityRepository +import com.dclass.backend.domain.user.User +import com.dclass.backend.domain.user.UserRepository +import jakarta.transaction.Transactional +import org.springframework.boot.CommandLineRunner +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Component +import java.time.LocalDateTime + +@Profile("local") +@Transactional +@Component +class DatabaseInitializer( + private val database: Database, + private val universityRepository: UniversityRepository, + private val departmentRepository: DepartmentRepository, + private val communityRepository: CommunityRepository, + private val belongRepository: BelongRepository, + private val userRepository: UserRepository, + private val postRepository: PostRepository, + private val commentRepository: CommentRepository, + private val replyRepository: ReplyRepository, + private val commentService: CommentService, + private val replyService: ReplyService, + private val postService: PostService, +) : CommandLineRunner { + + override fun run(vararg args: String) { + cleanUp() + populate() + } + + private fun cleanUp() { + database.clear(database.retrieveTables()) + } + + private fun populate() { + populateUniversity() + populateDepartment() + populateCommunity() + populateUser() +// populateDummyPosts() +// populateDummyComments() +// populateDummyReplies() + } + + + private fun populateUniversity() { + val universities = listOf( + University(name = "가천길대학", emailSuffix = "gachon.ac.kr"), + University(name = "가톨릭상지대학교", emailSuffix = "csj.ac.kr"), + University(name = "강동대학교", emailSuffix = "gangdong.ac.kr"), + University(name = "강릉영동대학교", emailSuffix = "gyc.ac.kr"), + University(name = "강원관광대학", emailSuffix = "kt.ac.kr"), + University(name = "강원도립대학", emailSuffix = "gw.ac.kr"), + University(name = "거제대학교", emailSuffix = "koje.ac.kr"), + University(name = "경기과학기술대학교", emailSuffix = "gtec.ac.kr"), + University(name = "경남도립거창대학", emailSuffix = "gc.ac.kr"), + University(name = "경남도립남해대학", emailSuffix = "namhae.ac.kr"), + University(name = "경남정보대학교", emailSuffix = "kit.ac.kr"), + University(name = "경민대학교", emailSuffix = "kyungmin.ac.kr"), + University(name = "경복대학교", emailSuffix = "kbu.ac.kr"), + University(name = "경북과학대학교", emailSuffix = "kbsc.ac.kr"), + University(name = "경북도립대학교", emailSuffix = "gpc.ac.kr"), + University(name = "경북전문대학교", emailSuffix = "kbc.ac.kr"), + University(name = "경산1대학교", emailSuffix = "gs.ac.kr"), + University(name = "경원전문대학", emailSuffix = "kwc.ac.kr"), + University(name = "경인여자대학교", emailSuffix = "kic.ac.kr"), + University(name = "계명문화대학교", emailSuffix = "kmcu.ac.kr"), + University(name = "계원예술대학교", emailSuffix = "kaywon.ac.kr"), + University(name = "고구려대학교", emailSuffix = "kgrc.ac.kr"), + University(name = "광양보건대학교", emailSuffix = "kwangyang.ac.kr"), + University(name = "광주보건대학교", emailSuffix = "ghu.ac.kr"), + University(name = "구미대학교", emailSuffix = "gumi.ac.kr"), + University(name = "구세군사관학교", emailSuffix = "saotc.ac.kr"), + University(name = "국제대학교", emailSuffix = "kookje.ac.kr"), + University(name = "군산간호대학교", emailSuffix = "kcn.ac.kr"), + University(name = "군장대학교", emailSuffix = "kunjang.ac.kr"), + University(name = "기독간호대학교", emailSuffix = "ccn.ac.kr"), + University(name = "김천과학대학", emailSuffix = "kcs.ac.kr"), + University(name = "김천대학", emailSuffix = "gimcheon.ac.kr"), + University(name = "김포대학교", emailSuffix = "kimpo.ac.kr"), + University(name = "김해대학교", emailSuffix = "gimhae.ac.kr"), + University(name = "농협대학교", emailSuffix = "nonghyup.ac.kr"), + University(name = "대경대학교", emailSuffix = "tk.ac.kr"), + University(name = "대구공업대학교", emailSuffix = "ttc.ac.kr"), + University(name = "대구과학대학교", emailSuffix = "tsu.ac.kr"), + University(name = "대구미래대학교", emailSuffix = "dfc.ac.kr"), + University(name = "대구보건대학교", emailSuffix = "dhc.ac.kr"), + University(name = "대덕대학교", emailSuffix = "ddu.ac.kr"), + University(name = "대동대학교", emailSuffix = "daedong.ac.kr"), + University(name = "대림대학교", emailSuffix = "daelim.ac.kr"), + University(name = "대원대학교", emailSuffix = "daewon.ac.kr"), + University(name = "대전보건대학교", emailSuffix = "hit.ac.kr"), + University(name = "동강대학교", emailSuffix = "dkc.ac.kr"), + University(name = "동남보건대학교", emailSuffix = "dongnam.ac.kr"), + University(name = "동명대학", emailSuffix = "tu.ac.kr"), + University(name = "동부산대학교", emailSuffix = "dpc.ac.kr"), + University(name = "동서울대학교", emailSuffix = "dsc.ac.kr"), + University(name = "동아방송예술대학교", emailSuffix = "dima.ac.kr"), + University(name = "동아인재대학교", emailSuffix = "dongac.ac.kr"), + University(name = "동양미래대학교", emailSuffix = "dongyang.ac.kr"), + University(name = "동우대학", emailSuffix = "duc.ac.kr"), + University(name = "동원과학기술대학교", emailSuffix = "dist.ac.kr"), + University(name = "동원대학교", emailSuffix = "tw.ac.kr"), + University(name = "동의과학대학교", emailSuffix = "dit.ac.kr"), + University(name = "동주대학교", emailSuffix = "dongju.ac.kr"), + University(name = "두원공과대학교", emailSuffix = "doowon.ac.kr"), + University(name = "마산대학교", emailSuffix = "masan.ac.kr"), + University(name = "명지전문대학", emailSuffix = "mjc.ac.kr"), + University(name = "목포과학대학교", emailSuffix = "mokpo-c.ac.kr"), + University(name = "문경대학교", emailSuffix = "mkc.ac.kr"), + University(name = "배화여자대학교", emailSuffix = "baewha.ac.kr"), + University(name = "백석문화대학교", emailSuffix = "bscu.ac.kr"), + University(name = "백제예술대학교", emailSuffix = "paekche.ac.kr"), + University(name = "벽성대학", emailSuffix = "bs.ac.kr"), + University(name = "부산경상대학교", emailSuffix = "bsks.ac.kr"), + University(name = "부산과학기술대학교", emailSuffix = "bist.ac.kr"), + University(name = "부산여자대학교", emailSuffix = "bwc.ac.kr"), + University(name = "부산예술대학교", emailSuffix = "busanarts.ac.kr"), + University(name = "부천대학교", emailSuffix = "bc.ac.kr"), + University(name = "삼육보건대학", emailSuffix = "shu.ac.kr"), + University(name = "삼육의명대학", emailSuffix = "syu.ac.kr"), + University(name = "상지영서대학교", emailSuffix = "sy.ac.kr"), + University(name = "서라벌대학교", emailSuffix = "sorabol.ac.kr"), + University(name = "서영대학교", emailSuffix = "seoyeong.ac.kr"), + University(name = "서울보건대학", emailSuffix = "shjc.ac.kr"), + University(name = "서울여자간호대학교", emailSuffix = "snjc.ac.kr"), + University(name = "서울예술대학교", emailSuffix = "seoularts.ac.kr"), + University(name = "서일대학교", emailSuffix = "seoil.ac.kr"), + University(name = "서정대학교", emailSuffix = "seojeong.ac.kr"), + University(name = "서해대학", emailSuffix = "sohae.ac.kr"), + University(name = "선린대학교", emailSuffix = "sunlin.ac.kr"), + University(name = "성덕대학교", emailSuffix = "sdc.ac.kr"), + University(name = "성심외국어대학", emailSuffix = "sungsim.ac.kr"), + University(name = "세경대학교", emailSuffix = "saekyung.ac.kr"), + University(name = "송곡대학교", emailSuffix = "songgok.ac.kr"), + University(name = "송원대학", emailSuffix = "songwon.ac.kr"), + University(name = "송호대학교", emailSuffix = "songho.ac.kr"), + University(name = "수성대학교", emailSuffix = "sc.ac.kr"), + University(name = "수원과학대학교", emailSuffix = "ssc.ac.kr"), + University(name = "수원여자대학교", emailSuffix = "swc.ac.kr"), + University(name = "순천제일대학", emailSuffix = "suncheon.ac.kr"), + University(name = "숭의여자대학교", emailSuffix = "sewc.ac.kr"), + University(name = "신구대학교", emailSuffix = "shingu.ac.kr"), + University(name = "신성대학교", emailSuffix = "shinsung.ac.kr"), + University(name = "신안산대학교", emailSuffix = "sau.ac.kr"), + University(name = "신흥대학교", emailSuffix = "shc.ac.kr"), + University(name = "아주자동차대학", emailSuffix = "motor.ac.kr"), + University(name = "안동과학대학교", emailSuffix = "asc.ac.kr"), + University(name = "안산대학교", emailSuffix = "ansan.ac.kr"), + University(name = "여주대학교", emailSuffix = "yit.ac.kr"), + University(name = "연성대학교", emailSuffix = "yeonsung.ac.kr"), + University(name = "연암공과대학교", emailSuffix = "yc.ac.kr"), + University(name = "영남외국어대학", emailSuffix = "yflc.ac.kr"), + University(name = "영남이공대학교", emailSuffix = "ync.ac.kr"), + University(name = "영진사이버대학", emailSuffix = "ycc.ac.kr"), + University(name = "영진전문대학", emailSuffix = "yjc.ac.kr"), + University(name = "오산대학교", emailSuffix = "osan.ac.kr"), + University(name = "용인송담대학교", emailSuffix = "ysc.ac.kr"), + University(name = "우송공업대학", emailSuffix = "wst.ac.kr"), + University(name = "우송정보대학", emailSuffix = "wsi.ac.kr"), + University(name = "울산과학대학교", emailSuffix = "uc.ac.kr"), + University(name = "웅지세무대학", emailSuffix = "wat.ac.kr"), + University(name = "원광보건대학교", emailSuffix = "wkhc.ac.kr"), + University(name = "원주대학", emailSuffix = "wonju.ac.kr"), + University(name = "유한대학교", emailSuffix = "yuhan.ac.kr"), + University(name = "인덕대학교", emailSuffix = "induk.ac.kr"), + University(name = "인천재능대학교", emailSuffix = "jeiu.ac.kr"), + University(name = "인천전문대학", emailSuffix = "icc.ac.kr"), + University(name = "인하공업전문대학", emailSuffix = "itc.ac.kr"), + University(name = "장안대학교", emailSuffix = "jangan.ac.kr"), + University(name = "적십자간호대학", emailSuffix = "cau.ac.kr"), + University(name = "전남과학대학교", emailSuffix = "chunnam-c.ac.kr"), + University(name = "전남도립대학교", emailSuffix = "dorip.ac.kr"), + University(name = "전북과학대학교", emailSuffix = "jbsc.ac.kr"), + University(name = "전주기전대학", emailSuffix = "jk.ac.kr"), + University(name = "전주비전대학교", emailSuffix = "jvision.ac.kr"), + University(name = "제주관광대학교", emailSuffix = "ctc.ac.kr"), + University(name = "제주산업정보대학", emailSuffix = "jeju.ac.kr"), + University(name = "제주한라대학교", emailSuffix = "chu.ac.kr"), + University(name = "조선간호대학교", emailSuffix = "cnc.ac.kr"), + University(name = "조선이공대학교", emailSuffix = "cst.ac.kr"), + University(name = "진주보건대학교", emailSuffix = "jhc.ac.kr"), + University(name = "창신대학", emailSuffix = "csc.ac.kr"), + University(name = "창원문성대학", emailSuffix = "cmu.ac.kr"), + University(name = "천안연암대학", emailSuffix = "yonam.ac.kr"), + University(name = "청강문화산업대학교", emailSuffix = "chungkang.academy"), + University(name = "청암대학교", emailSuffix = "scjc.ac.kr"), + University(name = "춘해보건대학교", emailSuffix = "ch.ac.kr"), + University(name = "충남도립청양대학", emailSuffix = "cyc.ac.kr"), + University(name = "충북도립대학", emailSuffix = "cpu.ac.kr"), + University(name = "충북보건과학대학교", emailSuffix = "chsu.ac.kr"), + University(name = "충청대학교", emailSuffix = "ok.ac.kr"), + University(name = "포항대학교", emailSuffix = "pohang.ac.kr"), + University(name = "한국골프대학", emailSuffix = "kg.ac.kr"), + University(name = "한국관광대학교", emailSuffix = "ktc.ac.kr"), + University(name = "한국농수산대학", emailSuffix = "af.ac.kr"), + University(name = "한국복지대학교", emailSuffix = "hanrw.ac.kr"), + University(name = "한국복지사이버대학", emailSuffix = "corea.ac.kr"), + University(name = "한국승강기대학교", emailSuffix = "klc.ac.kr"), + University(name = "한국영상대학교", emailSuffix = "pro.ac.kr"), + University(name = "한국정보통신기능대학", emailSuffix = "icpc.ac.kr"), + University(name = "한국철도대학", emailSuffix = "krc.ac.kr"), + University(name = "한국폴리텍", emailSuffix = "kopo.ac.kr "), + University(name = "한림성심대학교", emailSuffix = "hsc.ac.kr"), + University(name = "한양여자대학교", emailSuffix = "hywoman.ac.kr"), + University(name = "한영대학", emailSuffix = "hanyeong.ac.kr"), + University(name = "혜전대학", emailSuffix = "hj.ac.kr"), + University(name = "혜천대학교", emailSuffix = "hu.ac.kr"), + University(name = "가야대학교", emailSuffix = "kaya.ac.kr"), + University(name = "가천대학교", emailSuffix = "gachon.ac.kr"), + University(name = "가천의과학대학교", emailSuffix = "gachon.ac.kr"), + University(name = "가톨릭대학교", emailSuffix = "catholic.ac.kr"), + University(name = "감리교신학대학교", emailSuffix = "mtu.ac.kr"), + University(name = "강남대학교", emailSuffix = "kangnam.ac.kr"), + University(name = "강릉원주대학교", emailSuffix = "gwnu.ac.kr"), + University(name = "강원대학교", emailSuffix = "kangwon.ac.kr"), + University(name = "건국대학교", emailSuffix = "konkuk.ac.kr"), + University(name = "건국대학교(글로컬)", emailSuffix = "kku.ac.kr"), + University(name = "건양대학교", emailSuffix = "konyang.ac.kr"), + University(name = "건양사이버대학교", emailSuffix = "kycu.ac.kr"), + University(name = "경기대학교", emailSuffix = "kyonggi.ac.kr"), + University(name = "경남과학기술대학교", emailSuffix = "gntech.ac.kr"), + University(name = "경남대학교", emailSuffix = "hanma.kr"), + University(name = "경동대학교", emailSuffix = "k1.ac.kr"), + University(name = "경북대학교", emailSuffix = "knu.ac.kr"), + University(name = "경북외국어대학교", emailSuffix = "kufs.ac.kr"), + University(name = "경상대학교", emailSuffix = "gnu.ac.kr"), + University(name = "경성대학교", emailSuffix = "ks.ac.kr"), + University(name = "경운대학교", emailSuffix = "ikw.ac.kr"), + University(name = "경운대학교(산업대)", emailSuffix = "ikw.ac.kr"), + University(name = "경인교육대학교", emailSuffix = "ginue.ac.kr"), + University(name = "경일대학교", emailSuffix = "kiu.kr"), + University(name = "경주대학교", emailSuffix = "gju.ac.kr"), + University(name = "경희대학교", emailSuffix = "khu.ac.kr"), + University(name = "경희사이버대학교", emailSuffix = "khcu.ac.kr"), + University(name = "계명대학교", emailSuffix = "kmu.ac.kr"), + University(name = "고려대학교", emailSuffix = "korea.ac.kr"), + University(name = "고려대학교(세종)", emailSuffix = "korea.ac.kr"), + University(name = "고려사이버대학교", emailSuffix = "cuk.edu"), + University(name = "고신대학교", emailSuffix = "kosin.ac.kr"), + University(name = "공주교육대학교", emailSuffix = "gjue.ac.kr"), + University(name = "공주대학교", emailSuffix = "smail.kongju.ac.kr"), + University(name = "가톨릭관동대학교", emailSuffix = "cku.ac.kr"), + University(name = "광신대학교", emailSuffix = "kwangshin.ac.kr"), + University(name = "광운대학교", emailSuffix = "kw.ac.kr"), + University(name = "광주가톨릭대학교", emailSuffix = "kjcatholic.ac.kr"), + University(name = "GIST", emailSuffix = "gist.ac.kr"), + University(name = "광주교육대학교", emailSuffix = "gnue.ac.kr"), + University(name = "광주대학교", emailSuffix = "gwangju.ac.kr"), + University(name = "광주대학교(산업대)", emailSuffix = "gwangju.ac.kr"), + University(name = "광주여자대학교", emailSuffix = "kwu.ac.kr"), + University(name = "국민대학교", emailSuffix = "kookmin.ac.kr"), + University(name = "국제사이버대학교", emailSuffix = "gcu.ac"), + University(name = "군산대학교", emailSuffix = "kunsan.ac.kr"), + University(name = "그리스도대학교", emailSuffix = "kcu.ac.kr"), + University(name = "극동대학교", emailSuffix = "kdu.ac.kr"), + University(name = "글로벌사이버대학교", emailSuffix = "global.ac.kr"), + University(name = "금강대학교", emailSuffix = "ggu.ac.kr"), + University(name = "금오공과대학교", emailSuffix = "kumoh.ac.kr"), + University(name = "김천대학교", emailSuffix = "gimcheon.ac.kr"), + University(name = "꽃동네대학교", emailSuffix = "kkot.ac.kr"), + University(name = "나사렛대학교", emailSuffix = "kornu.ac.kr"), + University(name = "남부대학교", emailSuffix = "nambu.ac.kr"), + University(name = "남서울대학교", emailSuffix = "nsu.ac.kr"), + University(name = "남서울대학교(산업대)", emailSuffix = "nsu.ac.kr"), + University(name = "단국대학교", emailSuffix = "dankook.ac.kr"), + University(name = "대구가톨릭대학교", emailSuffix = "cu.ac.kr"), + University(name = "DGIST", emailSuffix = "dgist.ac.kr"), + University(name = "대구교육대학교", emailSuffix = "dnue.ac.kr"), + University(name = "대구대학교", emailSuffix = "daegu.ac.kr"), + University(name = "대구사이버대학교", emailSuffix = "dcu.ac.kr"), + University(name = "대구예술대학교", emailSuffix = "dgau.ac.kr"), + University(name = "대구외국어대학교", emailSuffix = "dufs.ac.kr"), + University(name = "대구한의대학교", emailSuffix = "dhu.ac.kr"), + University(name = "대신대학교", emailSuffix = "daeshin.ac.kr"), + University(name = "대전가톨릭대학교", emailSuffix = "dcatholic.ac.kr"), + University(name = "대전대학교", emailSuffix = "edu.dju.ac.kr"), + University(name = "대전신학교", emailSuffix = "daejeon.ac.kr"), + University(name = "대전신학대학교", emailSuffix = "daejeon.ac.kr"), + University(name = "대진대학교", emailSuffix = "daejin.ac.kr"), + University(name = "덕성여자대학교", emailSuffix = "duksung.ac.kr"), + University(name = "동국대학교", emailSuffix = "dongguk.edu"), + University(name = "동국대학교(경주)", emailSuffix = "dongguk.ac.kr"), + University(name = "동덕여자대학교", emailSuffix = "dongduk.ac.kr"), + University(name = "동명대학교", emailSuffix = "tu.ac.kr"), + University(name = "동명정보대학교", emailSuffix = "tu.ac.kr"), + University(name = "동서대학교", emailSuffix = "dongseo.ac.kr"), + University(name = "동신대학교", emailSuffix = "dsu.kr"), + University(name = "동아대학교", emailSuffix = "donga.ac.kr"), + University(name = "동양대학교", emailSuffix = "dyu.ac.kr"), + University(name = "동의대학교", emailSuffix = "deu.ac.kr"), + University(name = "디지털서울문화예술대학교", emailSuffix = "scau.ac.kr"), + University(name = "루터대학교", emailSuffix = "ltu.ac.kr"), + University(name = "명지대학교", emailSuffix = "mju.ac.kr"), + University(name = "목원대학교", emailSuffix = "mokwon.ac.kr"), + University(name = "목포가톨릭대학교", emailSuffix = "mcu.ac.kr"), + University(name = "목포대학교", emailSuffix = "mokpo.ac.kr"), + University(name = "목포해양대학교", emailSuffix = "mmu.ac.kr"), + University(name = "배재대학교", emailSuffix = "pcu.ac.kr"), + University(name = "백석대학교", emailSuffix = "bu.ac.kr"), + University(name = "부경대학교", emailSuffix = "pukyong.ac.kr"), + University(name = "부산가톨릭대학교", emailSuffix = "cup.ac.kr"), + University(name = "부산교육대학교", emailSuffix = "bnue.ac.kr"), + University(name = "부산대학교", emailSuffix = "pusan.ac.kr"), + University(name = "부산디지털대학교", emailSuffix = "bdu.ac.kr"), + University(name = "부산외국어대학교", emailSuffix = "bufs.ac.kr"), + University(name = "부산장신대학교", emailSuffix = "bpu.ac.kr"), + University(name = "사이버한국외국어대학교", emailSuffix = "cufs.ac.kr"), + University(name = "삼육대학교", emailSuffix = "syuin.ac.kr"), + University(name = "상명대학교", emailSuffix = "sangmyung.kr"), + University(name = "상명대학교(천안)", emailSuffix = "sangmyung.kr"), + University(name = "상주대학교", emailSuffix = "knu.ac.kr"), + University(name = "상지대학교", emailSuffix = "sangji.ac.kr"), + University(name = "서강대학교", emailSuffix = "sogang.ac.kr"), + University(name = "서경대학교", emailSuffix = "skuniv.ac.kr"), + University(name = "서남대학교", emailSuffix = "seonam.ac.kr"), + University(name = "서울과학기술대학교", emailSuffix = "seoultech.ac.kr"), + University(name = "서울과학기술대학교(산업대)", emailSuffix = "seoultech.ac.kr"), + University(name = "서울교육대학교", emailSuffix = "snue.ac.kr"), + University(name = "서울기독대학교", emailSuffix = "scu.ac.kr"), + University(name = "서울대학교", emailSuffix = "snu.ac.kr"), + University(name = "서울디지털대학교", emailSuffix = "sdu.ac.kr"), + University(name = "서울사이버대학교", emailSuffix = "iscu.ac.kr"), + University(name = "서울시립대학교", emailSuffix = "uos.ac.kr"), + University(name = "서울신학대학교", emailSuffix = "stu.ac.kr"), + University(name = "서울여자대학교", emailSuffix = "swu.ac.kr"), + University(name = "서울장신대학교", emailSuffix = "sjs.ac.kr"), + University(name = "서원대학교", emailSuffix = "seowon.ac.kr"), + University(name = "선문대학교", emailSuffix = "sunmoon.ac.kr"), + University(name = "성결대학교", emailSuffix = "sungkyul.ac.kr"), + University(name = "성공회대학교", emailSuffix = "skhu.ac.kr"), + University(name = "성균관대학교", emailSuffix = "skku.edu"), + University(name = "성신여자대학교", emailSuffix = "sungshin.ac.kr"), + University(name = "세명대학교", emailSuffix = "semyung.ac.kr"), + University(name = "세종대학교", emailSuffix = "sju.ac.kr"), + University(name = "세종사이버대학교", emailSuffix = "sjcu.ac.kr"), + University(name = "세한대학교", emailSuffix = "sehan.ac.kr"), + University(name = "송원대학교", emailSuffix = "songwon.ac.kr"), + University(name = "수원가톨릭대학교", emailSuffix = "suwoncatholic.ac.kr"), + University(name = "수원대학교", emailSuffix = "suwon.ac.kr"), + University(name = "숙명여자대학교", emailSuffix = "sookmyung.ac.kr"), + University(name = "순복음총회신학교", emailSuffix = "kcc.ac.kr"), + University(name = "순천대학교", emailSuffix = "scnu.ac.kr"), + University(name = "순천향대학교", emailSuffix = "sch.ac.kr"), + University(name = "숭실대학교", emailSuffix = "soongsil.ac.kr"), + University(name = "숭실사이버대학교", emailSuffix = "kcu.ac"), + University(name = "신경대학교", emailSuffix = "sgu.ac.kr"), + University(name = "신라대학교", emailSuffix = "silla.ac.kr"), + University(name = "아세아연합신학대학교", emailSuffix = "acts.ac.kr"), + University(name = "아주대학교", emailSuffix = "ajou.ac.kr"), + University(name = "안동대학교", emailSuffix = "anu.ac.kr"), + University(name = "안양대학교", emailSuffix = "ayum.anyang.ac.kr"), + University(name = "연세대학교", emailSuffix = "yonsei.ac.kr"), + University(name = "연세대학교(원주)", emailSuffix = "yonsei.ac.kr"), + University(name = "열린사이버대학교", emailSuffix = "ocu.ac.kr"), + University(name = "영남대학교", emailSuffix = "ynu.ac.kr"), + University(name = "영남신학대학교", emailSuffix = "ytus.ac.kr"), + University(name = "유원대학교", emailSuffix = "u1.ac.kr"), + University(name = "영산대학교", emailSuffix = "ysu.ac.kr"), + University(name = "영산대학교(산업대)", emailSuffix = "ysu.ac.kr"), + University(name = "영산선학대학교", emailSuffix = "youngsan.ac.kr"), + University(name = "예수대학교", emailSuffix = "jesus.ac.kr"), + University(name = "예원예술대학교", emailSuffix = "yewon.ac.kr"), + University(name = "용인대학교", emailSuffix = "yiu.ac.kr"), + University(name = "우석대학교", emailSuffix = "woosuk.ac.kr"), + University(name = "우송대학교", emailSuffix = "wsu.ac.kr"), + University(name = "우송대학교(산업대)", emailSuffix = "wsu.ac.kr"), + University(name = "UNIST", emailSuffix = "unist.ac.kr"), + University(name = "울산대학교", emailSuffix = "ulsan.ac.kr"), + University(name = "원광대학교", emailSuffix = "wonkwang.ac.kr"), + University(name = "원광디지털대학교", emailSuffix = "wdu.ac.kr"), + University(name = "위덕대학교", emailSuffix = "uu.ac.kr"), + University(name = "을지대학교", emailSuffix = "eulji.ac.kr"), + University(name = "이화여자대학교", emailSuffix = "ewhain.net"), + University(name = "인제대학교", emailSuffix = "inje.ac.kr"), + University(name = "인천가톨릭대학교", emailSuffix = "iccu.ac.kr"), + University(name = "인천대학교", emailSuffix = "inu.ac.kr"), + University(name = "인하대학교", emailSuffix = "inha.edu"), + University(name = "장로회신학대학교", emailSuffix = "pcts.ac.kr"), + University(name = "전남대학교", emailSuffix = "jnu.ac.kr"), + University(name = "전북대학교", emailSuffix = "jbnu.ac.kr"), + University(name = "전주교육대학교", emailSuffix = "jnue.kr"), + University(name = "전주대학교", emailSuffix = "jj.ac.kr"), + University(name = "정석대학", emailSuffix = "jit.ac.kr"), + University(name = "제주교육대학교", emailSuffix = "jejue.ac.kr"), + University(name = "제주국제대학교", emailSuffix = "jeju.ac.kr"), + University(name = "제주대학교", emailSuffix = "jejunu.ac.kr"), + University(name = "조선대학교", emailSuffix = "chosun.kr"), + University(name = "중부대학교", emailSuffix = "jmail.ac.kr"), + University(name = "중앙대학교", emailSuffix = "cau.ac.kr"), + University(name = "중앙대학교(안성)", emailSuffix = "cau.ac.kr"), + University(name = "중앙승가대학교", emailSuffix = "sangha.ac.kr"), + University(name = "중원대학교", emailSuffix = "jwu.ac.kr"), + University(name = "진주교육대학교", emailSuffix = "cue.ac.kr"), + University(name = "진주산업대학교(산업대)", emailSuffix = "gntech.ac.kr"), + University(name = "차의과학대학교", emailSuffix = "cha.ac.kr"), + University(name = "창신대학교", emailSuffix = "cs.ac.kr"), + University(name = "창원대학교", emailSuffix = "changwon.ac.kr"), + University(name = "청운대학교", emailSuffix = "chungwoon.ac.kr"), + University(name = "청주교육대학교", emailSuffix = "cje.ac.kr"), + University(name = "청주대학교", emailSuffix = "cju.ac.kr"), + University(name = "초당대학교", emailSuffix = "chodang.ac.kr"), + University(name = "초당대학교(산업대)", emailSuffix = "chodang.ac.kr"), + University(name = "총신대학교", emailSuffix = "chongshin.ac.kr"), + University(name = "추계예술대학교", emailSuffix = "chugye.ac.kr"), + University(name = "춘천교육대학교", emailSuffix = "cnue.ac.kr"), + University(name = "충남대학교", emailSuffix = "cnu.ac.kr"), + University(name = "충북대학교", emailSuffix = "chungbuk.ac.kr"), + University(name = "침례신학대학교", emailSuffix = "kbtus.ac.kr"), + University(name = "칼빈대학교", emailSuffix = "calvin.ac.kr"), + University(name = "탐라대학교", emailSuffix = "tnu.ac.kr"), + University(name = "평택대학교", emailSuffix = "ptu.ac.kr"), + University(name = "POSTECH", emailSuffix = "postech.ac.kr"), + University(name = "한경대학교", emailSuffix = "hknu.ac.kr"), + University(name = "한경대학교(산업대)", emailSuffix = "hknu.ac.kr"), + University(name = "KAIST", emailSuffix = "kaist.ac.kr"), + University(name = "한국교원대학교", emailSuffix = "knue.ac.kr"), + University(name = "한국교통대학교", emailSuffix = "ut.ac.kr"), + University(name = "한국교통대학교(산업대)", emailSuffix = "ut.ac.kr"), + University(name = "한국국제대학교", emailSuffix = "iuk.ac.kr"), + University(name = "한국기술교육대학교", emailSuffix = "koreatech.ac.kr"), + University(name = "한국방송통신대학교", emailSuffix = "knou.ac.kr"), + University(name = "한국산업기술대학교", emailSuffix = "kpu.ac.kr"), + University(name = "한국산업기술대학교(산업대)", emailSuffix = "kpu.ac.kr"), + University(name = "한국성서대학교", emailSuffix = "bible.ac.kr"), + University(name = "한국예술종합학교", emailSuffix = "karts.ac.kr"), + University(name = "한국외국어대학교", emailSuffix = "hufs.ac.kr"), + University(name = "한국전통문화대학교", emailSuffix = "nuch.ac.kr"), + University(name = "한국체육대학교", emailSuffix = "knsu.ac.kr"), + University(name = "한국항공대학교", emailSuffix = "kau.kr"), + University(name = "한국해양대학교", emailSuffix = "kmou.ac.kr"), + University(name = "한남대학교", emailSuffix = "hannam.ac.kr"), + University(name = "한동대학교", emailSuffix = "handong.edu"), + University(name = "한라대학교", emailSuffix = "halla.ac.kr"), + University(name = "한려대학교", emailSuffix = "hanlyo.ac.kr"), + University(name = "한려대학교(산업대)", emailSuffix = "hanlyo.ac.kr"), + University(name = "한림대학교", emailSuffix = "hallym.ac.kr"), + University(name = "한민학교", emailSuffix = "hanmin.ac.kr"), + University(name = "한밭대학교", emailSuffix = "hanbat.ac.kr"), + University(name = "한밭대학교(산업대)", emailSuffix = "hanbat.ac.kr"), + University(name = "한북대학교", emailSuffix = "hanbuk.ac.kr"), + University(name = "한서대학교", emailSuffix = "hanseo.ac.kr"), + University(name = "한성대학교", emailSuffix = "hansung.ac.kr"), + University(name = "한세대학교", emailSuffix = "uohs.ac.kr"), + University(name = "한신대학교", emailSuffix = "hs.ac.kr"), + University(name = "한양대학교", emailSuffix = "hanyang.ac.kr"), + University(name = "한양대학교(ERICA)", emailSuffix = "hanyang.ac.kr"), + University(name = "한양사이버대학교", emailSuffix = "hycu.ac.kr"), + University(name = "한영신학대학교", emailSuffix = "hytu.ac.kr"), + University(name = "한일장신대학교", emailSuffix = "hanil.ac.kr"), + University(name = "한중대학교", emailSuffix = "hanzhong.ac.kr"), + University(name = "협성대학교", emailSuffix = "uhs.ac.kr"), + University(name = "호남대학교", emailSuffix = "honam.ac.kr"), + University(name = "호남신학대학교", emailSuffix = "htus.ac.kr"), + University(name = "호서대학교", emailSuffix = "hoseo.edu"), + University(name = "호원대학교", emailSuffix = "howon.ac.kr"), + University(name = "홍익대학교", emailSuffix = "hongik.ac.kr"), + University(name = "홍익대학교(세종)", emailSuffix = "hongik.ac.kr"), + University(name = "화신사이버대학교", emailSuffix = "hscu.ac.kr") + ) + universityRepository.saveAll(universities) + } + + private fun populateDepartment() { + val departments = listOf( + Department(title = "언어정보학과"), + Department(title = "한국어문학과"), + Department(title = "독어독문학과"), + Department(title = "노어노문학과"), + Department(title = "영어영문학과"), + Department(title = "일어일문학과"), + Department(title = "중어중문학과"), + Department(title = "불어불문학과"), + Department(title = "서어서문학과"), + Department(title = "북한학과"), + Department(title = "철학과"), + Department(title = "사학과"), + Department(title = "문화인류학과"), + Department(title = "문예창작학과"), + Department(title = "문헌정보학과"), + Department(title = "관광학과"), + Department(title = "한문학과"), + Department(title = "신학과"), + Department(title = "불교학과"), + Department(title = "자율전공학과"), + Department(title = "경영학과"), + Department(title = "경제학과"), + Department(title = "경영정보학과"), + Department(title = "국제통상학과"), + Department(title = "광고홍보학과"), + Department(title = "금융학과"), + Department(title = "회계학과"), + Department(title = "세무학과"), + Department(title = "심리학과"), + Department(title = "법학과"), + Department(title = "사회학과"), + Department(title = "도시학과"), + Department(title = "정치외교학과"), + Department(title = "국제학과"), + Department(title = "사회복지학과"), + Department(title = "미디어커뮤니케이션학과"), + Department(title = "지리학과"), + Department(title = "행정학과"), + Department(title = "군사학과"), + Department(title = "경찰행정학과"), + Department(title = "아동가족학과"), + Department(title = "소비자학과"), + Department(title = "물류학과"), + Department(title = "무역학과"), + Department(title = "호텔경영학과"), + Department(title = "가정교육과"), + Department(title = "건설공학교육과"), + Department(title = "과학교육과"), + Department(title = "전기전자통신공학교육과"), + Department(title = "기계재료공학교육과"), + Department(title = "기술교육과"), + Department(title = "농업교육과"), + Department(title = "물리교육과"), + Department(title = "미술교육과"), + Department(title = "사회교육과"), + Department(title = "생물교육과"), + Department(title = "수학교육과"), + Department(title = "수해양산업교육과"), + Department(title = "아동교육과"), + Department(title = "언어치료학과"), + Department(title = "언어교육학과"), + Department(title = "역사교육과"), + Department(title = "음악교육과"), + Department(title = "윤리교육과"), + Department(title = "종교교육과"), + Department(title = "지구과학교육과"), + Department(title = "지리교육과"), + Department(title = "체육교육과"), + Department(title = "초등교육과"), + Department(title = "컴퓨터교육과"), + Department(title = "특수교육과"), + Department(title = "한자교육과"), + Department(title = "화학교육과"), + Department(title = "환경교육과"), + Department(title = "컴퓨터공학과"), + Department(title = "조선공학과"), + Department(title = "산업공학과"), + Department(title = "멀티미디어학과"), + Department(title = "게임공학과"), + Department(title = "재료공학과"), + Department(title = "건축학과"), + Department(title = "물류시스템공학과"), + Department(title = "해양공학과"), + Department(title = "환경공학과"), + Department(title = "고분자공학과"), + Department(title = "광학공학과"), + Department(title = "교통공학과"), + Department(title = "국방기술학과"), + Department(title = "금속공학과"), + Department(title = "금형설계학과"), + Department(title = "기계공학과"), + Department(title = "나노공학과"), + Department(title = "도시공학과"), + Department(title = "로봇공학과"), + Department(title = "무인항공학과"), + Department(title = "반도체학과"), + Department(title = "섬유학과"), + Department(title = "세라믹공학과"), + Department(title = "소방방재학과"), + Department(title = "신소재공학과"), + Department(title = "신재생에너지공학과"), + Department(title = "안전공학과"), + Department(title = "원자력공학과"), + Department(title = "자동차공학과"), + Department(title = "전기전자공학과"), + Department(title = "철도교통학과"), + Department(title = "토목공학과"), + Department(title = "항공우주공학과"), + Department(title = "화학공학과"), + Department(title = "생명과학과"), + Department(title = "원예학과"), + Department(title = "조경학과"), + Department(title = "동물학과"), + Department(title = "약학공학과"), + Department(title = "식품과학과"), + Department(title = "수의학과"), + Department(title = "천문학과"), + Department(title = "물리학과"), + Department(title = "수학과"), + Department(title = "화학과"), + Department(title = "의류학과"), + Department(title = "산림공학과"), + Department(title = "지질학과"), + Department(title = "지리학과"), + Department(title = "의학보건계열"), + Department(title = "의예과"), + Department(title = "간호학과"), + Department(title = "약학과"), + Department(title = "치의학과"), + Department(title = "물리치료학과"), + Department(title = "한의학과"), + Department(title = "환경보건학과"), + Department(title = "긴급구조학과"), + Department(title = "보건행정학과"), + Department(title = "임상병리학과"), + Department(title = "방사선학과"), + Department(title = "미술치료학과"), + Department(title = "언어재활학과"), + Department(title = "응용미술학과"), + Department(title = "조소과"), + Department(title = "회화과"), + Department(title = "연극영화학과"), + Department(title = "그래픽디자인학과"), + Department(title = "영상디자인학과"), + Department(title = "산업디자인학과"), + Department(title = "공업디자인학과"), + Department(title = "공간디자인학과"), + Department(title = "시각디자인학과"), + Department(title = "금속공예학과"), + Department(title = "애니메이션학과"), + Department(title = "실용음악학과"), + Department(title = "성악학과"), + Department(title = "응용음악학과"), + Department(title = "패션디자인학과"), + Department(title = "실내디자인학과"), + Department(title = "광고디자인학과"), + Department(title = "무용스포츠학과"), + Department(title = "사회체육학과"), + Department(title = "건강관리학과"), + Department(title = "메이크업아티스트학과"), + Department(title = "모델학과"), + Department(title = "조리제빵학과"), + Department(id = 999, title = "") + ) + + departmentRepository.saveAll(departments) + + + } + + private fun populateUser() { + + val university = universityRepository.findById(205).get()!! + + + val users = listOf( + User( + name = "김덕배", + email = "duck@kookmin.ac.kr", + nickname = "duckduck", + password = "123123a", + university = university, + ), + User( + name = "홍길동", + email = "hong@kookmin.ac.kr", + nickname = "honggildong", + password = "123123a", + university = university, + ), + User( + name = "김철수", + email = "chulsu@kookmin.ac.kr", + nickname = "chulsukim", + password = "5678efgh", + university = university + ), + User( + name = "이영희", + email = "zerotwo@kookmin.ac.kr", + nickname = "zerotwo", + password = "9012ijkl", + university = university, + ), + User( + name = "박민수", + email = "minsu@kookmin.ac.kr", + nickname = "minsupark", + password = "3456mnop", + university = university, + ), + User( + name = "최영희", + email = "younghee@kookmin.ac.kr", + nickname = "youngheec", + password = "7890qrst", + university = university, + ), + User( + name = "김응수", + email = "yeswater@kookmin.ac.kr", + nickname = "yeswater", + password = "4567uvwx", + university = university, + ), + User( + name = "김민수", + email = "minsoo@kookmin.ac.kr", + nickname = "mskim", + password = "1234abcd", + university = university, + ), + User( + name = "이다은", + email = "daeun@kookmin.ac.kr", + nickname = "daeunlee", + password = "9012ijkl", + university = university, + ), + User( + name = "이승민", + email = "seungmin@kookmin.ac.kr", + nickname = "seungmine", + password = "7890qrst", + university = university, + ) + ) + + val savedUser = userRepository.saveAll(users) + + belongRepository.saveAll( + savedUser.map { + Belong(userId = it.id, ids = listOf(75, 163)) + } + ) + } + + private fun populateCommunity() { + + val departments = departmentRepository.findAll() + .forEach { + communityRepository.saveAll( + listOf( + Community(title = "FREE", departmentId = it.id), + Community(title = "GRADUATE", departmentId = it.id), + Community(title = "JOB", departmentId = it.id), + Community(title = "STUDY", departmentId = it.id), + Community(title = "QUESTION", departmentId = it.id), + Community(title = "PROMOTION", departmentId = it.id), + ) + ) + } + } + + + private fun populateDummyPosts() { + val users = userRepository.findAll() + val communityIds = (445L..450L).toList() + + + val dummyPosts = mutableListOf() + + var number = 1 + + for (user in users) { + for (i in 1..10) { + val communityId = communityIds.random() + val community = communityRepository.findById(communityId).get() + val post = createDummyPost(user, community, number) + dummyPosts.add(post) + number++ + } + } + + postRepository.saveAll(dummyPosts) + } + + private fun createDummyPost(user: User, community: Community, number: Int): Post { + val title = "Dummy Post Title ${number}" + val content = "Dummy Post Content" + val createdDateTime = LocalDateTime.now() + + return Post(user.id, community.id, title, content, PostLikes(), createdDateTime = createdDateTime) + } + + private fun populateDummyComments() { + val users = userRepository.findAll() + val posts = postRepository.findAll() + + val dummyComments = mutableListOf() + + var num = 1 + for (user in users) { + for (post in posts) { + val comment = createDummyComment(user, post, num) + dummyComments.add(comment) + num++ + post.increaseCommentReplyCount() + } + } + + commentRepository.saveAll(dummyComments) + } + + private fun createDummyComment(user: User, post: Post, num: Int): Comment { + val content = "댓글 ${num}" + val createdDateTime = LocalDateTime.now() + + return Comment(user.id, post.id, content, createdDateTime = createdDateTime) + } + + private fun populateDummyReplies() { + val users = userRepository.findAll() + val comments = commentRepository.findAll() + val posts = postRepository.findAll() + + val dummyReplies = mutableListOf() + + var num = 1 + for (user in users) { + for (comment in comments) { + val reply = createDummyReply(user, comment, num) + dummyReplies.add(reply) + num++ + comment.increaseReplyCount() + + val post = posts.find { it.id == comment.postId } + post?.increaseCommentReplyCount() + } + } + + replyRepository.saveAll(dummyReplies) + } + + + private fun createDummyReply(user: User, comment: Comment, num: Int): Reply { + val content = "대댓글 ${num}" + val createdDateTime = LocalDateTime.now() + + return Reply(user.id, comment.id, content, createdDateTime = createdDateTime) + } + + +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/config/SwaggerConfig.kt b/backend/src/main/kotlin/com/dclass/backend/config/SwaggerConfig.kt new file mode 100644 index 0000000000..6b7d0cf91e --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/config/SwaggerConfig.kt @@ -0,0 +1,49 @@ +package com.dclass.backend.config + +import io.swagger.v3.oas.models.Components +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.info.Info +import io.swagger.v3.oas.models.security.SecurityScheme +import io.swagger.v3.oas.models.servers.Server +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +const val ACCESS_TOKEN_SECURITY_SCHEME_KEY = "AccessToken(Bearer)" + +@Configuration +class SwaggerConfig { + + @Bean + fun openAPI(): OpenAPI { + return OpenAPI() + .components(authComponents()) + .addServersItem(server()) + .info(info()) + } + + private fun server(): Server { + return Server().url("/") + } + + private fun authComponents(): Components { + return Components().addSecuritySchemes( + ACCESS_TOKEN_SECURITY_SCHEME_KEY, + accessTokenSecurityScheme() + ) + } + + private fun accessTokenSecurityScheme(): SecurityScheme { + return SecurityScheme() + .name(ACCESS_TOKEN_SECURITY_SCHEME_KEY) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + } + + private fun info(): Info { + return Info() + .title("디클래스 API 명세서") + .description("Swagger UI") + .version("0.0.1") + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/authenticationcode/AuthenticationCode.kt b/backend/src/main/kotlin/com/dclass/backend/domain/authenticationcode/AuthenticationCode.kt new file mode 100644 index 0000000000..b064ff76b3 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/authenticationcode/AuthenticationCode.kt @@ -0,0 +1,62 @@ +package com.dclass.backend.domain.authenticationcode + +import com.dclass.backend.exception.authenticationcode.AuthenticationCodeException +import com.dclass.backend.exception.authenticationcode.AuthenticationCodeExceptionType.* +import com.dclass.support.domain.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import org.hibernate.annotations.SQLDelete +import org.hibernate.annotations.SQLRestriction +import java.time.Duration +import java.time.LocalDateTime +import java.util.* + +@SQLDelete(sql = "update authentication_code set deleted = true where id = ?") +@SQLRestriction("deleted = false") +@Entity +class AuthenticationCode( + @Column(nullable = false) + val email: String, + + @Column(nullable = false, columnDefinition = "char(6)") + val code: String = UUID.randomUUID().toString().take(6), + + @Column(nullable = false) + var authenticated: Boolean = false, + + @Column(nullable = false) + val createdDateTime: LocalDateTime = LocalDateTime.now() +) : BaseEntity() { + + @Column(nullable = false) + private var deleted: Boolean = false + + private val expiryDateTime: LocalDateTime + get() = createdDateTime + EXPIRY_MINUTE_TIME + + fun authenticate(code: String) { + if (this.code != code) { + throw AuthenticationCodeException(NOT_EQUAL_CODE) + } + if (authenticated) { + throw AuthenticationCodeException(ALREADY_VERIFIED) + } + if (expiryDateTime < LocalDateTime.now()) { + throw AuthenticationCodeException(EXPIRED_CODE) + } + authenticated = true + } + + fun validate(code: String) { + if (this.code != code) { + throw AuthenticationCodeException(NOT_EQUAL_CODE) + } + if (!authenticated) { + throw AuthenticationCodeException(NOT_VERIFIED) + } + } + + companion object { + private val EXPIRY_MINUTE_TIME: Duration = Duration.ofMinutes(10L) + } +} diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/authenticationcode/AuthenticationCodeRepository.kt b/backend/src/main/kotlin/com/dclass/backend/domain/authenticationcode/AuthenticationCodeRepository.kt new file mode 100644 index 0000000000..b44bcec746 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/authenticationcode/AuthenticationCodeRepository.kt @@ -0,0 +1,11 @@ +package com.dclass.backend.domain.authenticationcode + +import org.springframework.data.jpa.repository.JpaRepository + +fun AuthenticationCodeRepository.getLastByEmail(email: String): AuthenticationCode { + return findFirstByEmailOrderByCreatedDateTimeDesc(email) + ?: throw IllegalArgumentException("인증 코드가 존재하지 않습니다. email: $email") +} +interface AuthenticationCodeRepository : JpaRepository { + fun findFirstByEmailOrderByCreatedDateTimeDesc(email: String): AuthenticationCode? +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/belong/Belong.kt b/backend/src/main/kotlin/com/dclass/backend/domain/belong/Belong.kt new file mode 100644 index 0000000000..5f936edad6 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/belong/Belong.kt @@ -0,0 +1,94 @@ +package com.dclass.backend.domain.belong + +import com.dclass.backend.exception.belong.BelongException +import com.dclass.backend.exception.belong.BelongExceptionType.CHANGE_INTERVAL_VIOLATION +import com.dclass.backend.exception.belong.BelongExceptionType.MAX_BELONG +import com.dclass.support.domain.BaseEntity +import jakarta.persistence.* +import org.hibernate.annotations.SQLDelete +import org.hibernate.annotations.SQLRestriction +import java.time.Duration +import java.time.LocalDateTime + +@SQLDelete(sql = "update belong set deleted = true where id = ?") +@SQLRestriction("deleted = false") +@Entity +@Table +class Belong( + @Column(nullable = false, unique = true) + val userId: Long, + + ids: List = emptyList(), + + modifiedDateTime: LocalDateTime = LocalDateTime.now(), + + id: Long = 0L +) : BaseEntity(id) { + + @Column(nullable = false) + private var deleted: Boolean = false + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "join_department") + private val _departmentIds: MutableList = ids.toMutableList() + + val departmentIds: List + get() = _departmentIds + + @Column(nullable = false) + var modifiedDateTime: LocalDateTime = modifiedDateTime + private set + + val activated: Long + get() = _departmentIds.first() + + val remainingTime: Duration + get() = Duration.between( + LocalDateTime.now(), + modifiedDateTime.plusDays(CHANGE_INTERVAL_DAYS) + ).takeUnless { it.isNegative } ?: Duration.ZERO + + @Column(nullable = false) + var majorIndex: Int = 0 + private set + + val major: Long + get() = _departmentIds[majorIndex] + + val minor: Long + get() = _departmentIds[1 - majorIndex] + + init { + if (departmentIds.size > 2) { + throw BelongException(MAX_BELONG) + } + } + + fun update(ids: List) { + if (ids.size > 2) { + throw BelongException(MAX_BELONG) + } + if (!periodValidation()) { + throw BelongException(CHANGE_INTERVAL_VIOLATION) + } + _departmentIds.clear() + _departmentIds.addAll(ids) + modifiedDateTime = LocalDateTime.now() + } + + fun contain(departmentId: Long) = departmentIds.contains(departmentId) + + fun switch() { + _departmentIds.reverse() + majorIndex = 1 - majorIndex + } + + private fun periodValidation(): Boolean { + val now = LocalDateTime.now() + return modifiedDateTime.isBefore(now.minusDays(CHANGE_INTERVAL_DAYS)) + } + + companion object { + const val CHANGE_INTERVAL_DAYS = 90L + } +} diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/belong/BelongRepository.kt b/backend/src/main/kotlin/com/dclass/backend/domain/belong/BelongRepository.kt new file mode 100644 index 0000000000..a7cc62e991 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/belong/BelongRepository.kt @@ -0,0 +1,14 @@ +package com.dclass.backend.domain.belong + +import com.dclass.backend.exception.belong.BelongException +import com.dclass.backend.exception.belong.BelongExceptionType +import org.springframework.data.jpa.repository.JpaRepository + +fun BelongRepository.getOrThrow(userId: Long): Belong = findByUserId(userId) + ?: throw BelongException(BelongExceptionType.NOT_FOUND_BELONG) + + + +interface BelongRepository : JpaRepository { + fun findByUserId(userId: Long): Belong? +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/blacklist/Blacklist.kt b/backend/src/main/kotlin/com/dclass/backend/domain/blacklist/Blacklist.kt new file mode 100644 index 0000000000..fccddffd7a --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/blacklist/Blacklist.kt @@ -0,0 +1,19 @@ +package com.dclass.backend.domain.blacklist + +import com.dclass.support.domain.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import org.hibernate.annotations.SQLDelete +import org.hibernate.annotations.SQLRestriction + +@SQLDelete(sql = "update blacklist set deleted = true where id = ?") +@SQLRestriction("deleted = false") +@Entity +class Blacklist( + @Column(nullable = false) + val invalidRefreshToken: String, + id: Long = 0L +) : BaseEntity(id) { + @Column(nullable = false) + private var deleted: Boolean = false +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/blacklist/BlacklistRepository.kt b/backend/src/main/kotlin/com/dclass/backend/domain/blacklist/BlacklistRepository.kt new file mode 100644 index 0000000000..43f472137e --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/blacklist/BlacklistRepository.kt @@ -0,0 +1,7 @@ +package com.dclass.backend.domain.blacklist + +import org.springframework.data.jpa.repository.JpaRepository + +interface BlacklistRepository : JpaRepository { + fun findByInvalidRefreshToken(token: String): Blacklist? +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/blocklist/Blocklist.kt b/backend/src/main/kotlin/com/dclass/backend/domain/blocklist/Blocklist.kt new file mode 100644 index 0000000000..b1a20d9906 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/blocklist/Blocklist.kt @@ -0,0 +1,38 @@ +package com.dclass.backend.domain.blocklist + +import com.dclass.backend.exception.blocklist.BlocklistException +import com.dclass.backend.exception.blocklist.BlocklistExceptionType +import com.dclass.support.domain.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import java.time.Duration +import java.time.LocalDateTime + +@Entity +class Blocklist( + + @Column(nullable = false) + val userId: Long, + + @Column(nullable = false) + val createdDateTime: LocalDateTime = LocalDateTime.now(), + + id: Long = 0L +) : BaseEntity(id) { + + val remainingTime: Duration + get() = Duration.between( + LocalDateTime.now(), + createdDateTime.plusDays(CHANGE_INTERVAL_DAYS) + ).takeUnless { it.isNegative } ?: Duration.ZERO + + fun isExpired(): Boolean = remainingTime.isZero + + fun validate() { + if (!isExpired()) throw BlocklistException(BlocklistExceptionType.BLOCKED_USER) + } + + companion object { + const val CHANGE_INTERVAL_DAYS = 90L + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/blocklist/BlocklistRepository.kt b/backend/src/main/kotlin/com/dclass/backend/domain/blocklist/BlocklistRepository.kt new file mode 100644 index 0000000000..74903a8fe0 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/blocklist/BlocklistRepository.kt @@ -0,0 +1,12 @@ +package com.dclass.backend.domain.blocklist + +import com.dclass.backend.exception.blocklist.BlocklistException +import com.dclass.backend.exception.blocklist.BlocklistExceptionType +import org.springframework.data.jpa.repository.JpaRepository + +fun BlocklistRepository.getLatestByUserIdOrThrow(userId: Long): Blocklist = findFirstByUserIdOrderByCreatedDateTimeDesc(userId) + ?: throw BlocklistException(BlocklistExceptionType.NOT_FOUND_USER) + +interface BlocklistRepository : JpaRepository { + fun findFirstByUserIdOrderByCreatedDateTimeDesc(userId: Long): Blocklist? +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/comment/Comment.kt b/backend/src/main/kotlin/com/dclass/backend/domain/comment/Comment.kt new file mode 100644 index 0000000000..d645479ebc --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/comment/Comment.kt @@ -0,0 +1,88 @@ +package com.dclass.backend.domain.comment + +import com.dclass.backend.exception.comment.CommentException +import com.dclass.backend.exception.comment.CommentExceptionType +import com.dclass.support.domain.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Embedded +import jakarta.persistence.Entity +import jakarta.persistence.Version +import org.hibernate.annotations.SQLDelete +import org.hibernate.annotations.SQLRestriction +import java.time.LocalDateTime + +@SQLDelete(sql = "update comment set deleted = true where id = ? and version = ?") +@SQLRestriction("deleted = false OR (deleted = true AND reply_count > 0)") +@Entity +class Comment( + @Column(nullable = false) + val userId: Long, + + @Column(nullable = false) + val postId: Long, + + content: String = "", + + commentLikes: CommentLikes = CommentLikes(), + + @Column(nullable = false) + val createdDateTime: LocalDateTime = LocalDateTime.now(), + + @Version + val version: Long = 0L, + + modifiedDateTime: LocalDateTime = LocalDateTime.now(), + + id: Long = 0L +) : BaseEntity(id) { + + @Column(nullable = false) + private var deleted: Boolean = false + + @Column(nullable = false, length = 255) + var content: String = content + private set + + @Column(nullable = false) + var modifiedDateTime: LocalDateTime = modifiedDateTime + private set + + @Embedded + var commentLikes: CommentLikes = commentLikes + private set + + @Column(nullable = false) + var replyCount: Int = 0 + private set + + val likeCount: Int + get() = commentLikes.count + + fun likedBy(userId: Long) = + commentLikes.findUserById(userId) + + fun like(userId: Long) { + if (this.userId == userId) { + throw CommentException(CommentExceptionType.SELF_LIKE) + } + commentLikes.add(userId) + } + + fun isDeleted() = deleted + + fun changeContent(content: String) { + this.content = content + modifiedDateTime = LocalDateTime.now() + } + + fun isEligibleForSSE(userId: Long) = this.userId != userId + + fun increaseReplyCount() { + replyCount++ + } + + fun decreaseReplyCount() { + replyCount-- + } + +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/comment/CommentLike.kt b/backend/src/main/kotlin/com/dclass/backend/domain/comment/CommentLike.kt new file mode 100644 index 0000000000..66d003c8a8 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/comment/CommentLike.kt @@ -0,0 +1,10 @@ +package com.dclass.backend.domain.comment + +import jakarta.persistence.Column +import jakarta.persistence.Embeddable + +@Embeddable +class CommentLike( + @Column(nullable = false) + val usersId: Long, +) \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/comment/CommentLikes.kt b/backend/src/main/kotlin/com/dclass/backend/domain/comment/CommentLikes.kt new file mode 100644 index 0000000000..0c4ede57ac --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/comment/CommentLikes.kt @@ -0,0 +1,29 @@ +package com.dclass.backend.domain.comment + +import jakarta.persistence.CollectionTable +import jakarta.persistence.ElementCollection +import jakarta.persistence.Embeddable +import jakarta.persistence.FetchType + +@Embeddable +class CommentLikes( + likes: List = emptyList() +) { + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "comment_likes") + private val _likes: MutableList = likes.toMutableList() + + val likes: List + get() = _likes + + val count: Int + get() = _likes.size + + fun add(userId: Long) { + _likes.removeIf { it.usersId == userId } + _likes.add(CommentLike(userId)) + } + + fun findUserById(userId: Long) = + _likes.any { it.usersId == userId } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/comment/CommentRepository.kt b/backend/src/main/kotlin/com/dclass/backend/domain/comment/CommentRepository.kt new file mode 100644 index 0000000000..03b6cf9b52 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/comment/CommentRepository.kt @@ -0,0 +1,16 @@ +package com.dclass.backend.domain.comment + +import com.dclass.backend.exception.comment.CommentException +import com.dclass.backend.exception.comment.CommentExceptionType.NOT_FOUND_COMMENT +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.repository.findByIdOrNull + +fun CommentRepository.getByIdOrThrow(commentId: Long): Comment { + return findByIdOrNull(commentId) ?: throw CommentException(NOT_FOUND_COMMENT) +} + +interface CommentRepository : JpaRepository, CommentRepositorySupport { + fun findCommentByIdAndUserId(commentId: Long, userId: Long): Comment? + fun findAllByUserId(userId: Long): List + fun findByIdAndUserId(commentId: Long, userId: Long): Comment? +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/comment/CommentRepositorySupport.kt b/backend/src/main/kotlin/com/dclass/backend/domain/comment/CommentRepositorySupport.kt new file mode 100644 index 0000000000..d077d5f320 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/comment/CommentRepositorySupport.kt @@ -0,0 +1,65 @@ +package com.dclass.backend.domain.comment + +import com.dclass.backend.application.dto.CommentScrollPageRequest +import com.dclass.backend.application.dto.CommentWithUserResponse +import com.dclass.backend.domain.reply.Reply +import com.dclass.backend.domain.user.User +import com.linecorp.kotlinjdsl.dsl.jpql.jpql +import com.linecorp.kotlinjdsl.render.jpql.JpqlRenderContext +import com.linecorp.kotlinjdsl.support.spring.data.jpa.extension.createQuery +import jakarta.persistence.EntityManager + +interface CommentRepositorySupport { + fun findCommentWithUserByPostId(request: CommentScrollPageRequest): List + fun countCommentReplyByPostId(postId: Long): Long +} + +class CommentRepositoryImpl( + private val em: EntityManager, + private val context: JpqlRenderContext +) : CommentRepositorySupport { + + override fun findCommentWithUserByPostId(request: CommentScrollPageRequest): List { + val query = jpql { + selectNew( + entity(Comment::class), + entity(User::class), + ).from( + entity(Comment::class), + join(User::class).on(path(Comment::userId).eq(path(User::id))), + ).whereAnd( + path(Comment::postId).eq(request.postId), + path(Comment::id).greaterThan(request.lastCommentId ?: Long.MIN_VALUE), + ).orderBy( + path(Comment::id).asc() + ) + } + + return em.createQuery(query, context).setMaxResults(request.size).resultList + } + + override fun countCommentReplyByPostId(postId: Long): Long { + val query = jpql { + val replyCount = expression(Long::class, "cnt") + + val subquery = select( + count(entity(Reply::class)) + ).from( + entity(Comment::class), + join(Reply::class).on(path(Comment::id).eq(path(Reply::commentId))) + ).where( + path(Comment::postId).eq(postId) + ).asSubquery().`as`(replyCount) + + select( + count(path(Comment::id)).plus(subquery) + ).from( + entity(Comment::class) + ).where( + path(Comment::postId).eq(postId) + ) + } + + return em.createQuery(query, context).singleResult + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/community/Community.kt b/backend/src/main/kotlin/com/dclass/backend/domain/community/Community.kt new file mode 100644 index 0000000000..99a63bddb7 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/community/Community.kt @@ -0,0 +1,33 @@ +package com.dclass.backend.domain.community + +import com.dclass.support.domain.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import org.hibernate.annotations.SQLDelete +import org.hibernate.annotations.SQLRestriction + +@SQLDelete(sql = "update community set deleted = true where id = ?") +@SQLRestriction("deleted = false") +@Entity +class Community( + @Column(nullable = false) + val departmentId: Long, + + title: String = "", + + description: String = "", + + id: Long = 0L, +) : BaseEntity(id) { + + @Column(nullable = false) + private var deleted: Boolean = false + + @Column(nullable = false, length = 100) + var title: String = title + private set + + @Column(nullable = false, length = 255) + var description: String = description + private set +} diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/community/CommunityRepository.kt b/backend/src/main/kotlin/com/dclass/backend/domain/community/CommunityRepository.kt new file mode 100644 index 0000000000..4ac818d1a0 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/community/CommunityRepository.kt @@ -0,0 +1,18 @@ +package com.dclass.backend.domain.community + +import com.dclass.backend.exception.community.CommunityException +import com.dclass.backend.exception.community.CommunityExceptionType.NOT_FOUND_COMMUNITY +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.repository.findByIdOrNull + + +fun CommunityRepository.findByIdOrThrow(id: Long): Community { + return findByIdOrNull(id) ?: throw CommunityException(NOT_FOUND_COMMUNITY) +} + +interface CommunityRepository : JpaRepository { + fun findByDepartmentIdIn(departmentIds: List): List + fun findByDepartmentId(departmentId: Long): List + fun findByTitle(title: String): Community? + fun findByDepartmentIdAndTitle(departmentId: Long, title: String): Community? +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/community/CommunityType.kt b/backend/src/main/kotlin/com/dclass/backend/domain/community/CommunityType.kt new file mode 100644 index 0000000000..faeba2766f --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/community/CommunityType.kt @@ -0,0 +1,19 @@ +package com.dclass.backend.domain.community + +import com.dclass.backend.exception.community.CommunityException +import com.dclass.backend.exception.community.CommunityExceptionType.NOT_FOUND_COMMUNITY + + +enum class CommunityType(val value: String) { + FREE("자유게시판"), GRADUATE("대학원게시판"), JOB("취준게시판"), STUDY("스터디모집"), QUESTION("질문게시판"), PROMOTION("홍보게시판"); + + companion object { + fun from(value: String?): CommunityType? { + if (value.isNullOrBlank()) { + return null; + } + return enumValues().find { it.name == value } + ?: throw CommunityException(NOT_FOUND_COMMUNITY) + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/department/Department.kt b/backend/src/main/kotlin/com/dclass/backend/domain/department/Department.kt new file mode 100644 index 0000000000..19defa9e7a --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/department/Department.kt @@ -0,0 +1,20 @@ +package com.dclass.backend.domain.department + +import com.dclass.support.domain.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import org.hibernate.annotations.SQLDelete +import org.hibernate.annotations.SQLRestriction + +@SQLDelete(sql = "update department set deleted = true where id = ?") +@SQLRestriction("deleted = false") +@Entity +class Department( + @Column(nullable = false) + val title: String = "", + id: Long = 0L +) : BaseEntity(id) { + + @Column(nullable = false) + private var deleted: Boolean = false +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/department/DepartmentRepository.kt b/backend/src/main/kotlin/com/dclass/backend/domain/department/DepartmentRepository.kt new file mode 100644 index 0000000000..754fe84439 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/department/DepartmentRepository.kt @@ -0,0 +1,15 @@ +package com.dclass.backend.domain.department + +import com.dclass.backend.exception.department.DepartmentException +import com.dclass.backend.exception.department.DepartmentExceptionType.NOT_FOUND_DEPARTMENT +import org.springframework.data.jpa.repository.JpaRepository + +fun DepartmentRepository.getByTitleOrThrow(title: String): Department = findByTitle(title) + ?: throw DepartmentException(NOT_FOUND_DEPARTMENT) + +fun DepartmentRepository.getByIdOrThrow(id: Long): Department = + findById(id).orElseThrow { DepartmentException(NOT_FOUND_DEPARTMENT) } + +interface DepartmentRepository : JpaRepository { + fun findByTitle(title: String): Department? +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/emitter/EmitterRepository.kt b/backend/src/main/kotlin/com/dclass/backend/domain/emitter/EmitterRepository.kt new file mode 100644 index 0000000000..a05c9055e2 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/emitter/EmitterRepository.kt @@ -0,0 +1,51 @@ +package com.dclass.backend.domain.emitter + +import com.dclass.support.util.logger +import org.springframework.stereotype.Repository +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter +import java.util.concurrent.ConcurrentHashMap + +@Repository +class EmitterRepository { + private val emitters = ConcurrentHashMap() + private val eventCache = ConcurrentHashMap() + private val log = logger() + + fun save(userId: String, emitter: SseEmitter): SseEmitter { + emitters[userId] = emitter + return emitter + } + + fun saveEventCache(userId: String, event: Any) { + eventCache[userId] = event + } + + fun findAllEmitterStartWithByUserId(userId: String): Map { + return emitters.filter { it.key.startsWith(userId) } + } + + fun findAllEventCacheStartWithByUserId(userId: String): Map { + return eventCache.filter { it.key.startsWith(userId) } + } + + fun findAll(): Map { + return emitters + } + + fun delete(userId: String) { + log.info("delete emitter: $userId") + emitters.remove(userId) + } + + fun deleteAllEmitterStartWithByUserId(userId: String) { + emitters.keys.filter { it.startsWith(userId) }.forEach { emitters.remove(it) } + } + + fun deleteAllEventCacheStartWithByUserId(userId: String) { + eventCache.keys.filter { it.startsWith(userId) }.forEach { eventCache.remove(it) } + } + + fun get(userId: String): SseEmitter? { + return emitters[userId] + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/notification/Notification.kt b/backend/src/main/kotlin/com/dclass/backend/domain/notification/Notification.kt new file mode 100644 index 0000000000..70476980a6 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/notification/Notification.kt @@ -0,0 +1,34 @@ +package com.dclass.backend.domain.notification + +import com.dclass.support.domain.BaseEntity +import jakarta.persistence.* +import java.time.LocalDateTime + +@Entity +@Table(name = "notifications") +class Notification( + @Column(nullable = false) + val userId: Long, + + @Column(nullable = false) + val postId: Long, + + @Column(nullable = false) + val content: String, + + @Column(nullable = false) + var isRead: Boolean = false, + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + val type: NotificationType, + + @Column(nullable = false) + val createdAt: LocalDateTime = LocalDateTime.now(), + + id: Long = 0L +) : BaseEntity(id) { + fun read() { + isRead = true + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/notification/NotificationEvent.kt b/backend/src/main/kotlin/com/dclass/backend/domain/notification/NotificationEvent.kt new file mode 100644 index 0000000000..295ff97674 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/notification/NotificationEvent.kt @@ -0,0 +1,53 @@ +package com.dclass.backend.domain.notification + +import com.dclass.backend.domain.comment.Comment +import com.dclass.backend.domain.community.Community +import com.dclass.backend.domain.notification.NotificationType.COMMENT +import com.dclass.backend.domain.notification.NotificationType.REPLY +import com.dclass.backend.domain.post.Post +import com.dclass.backend.domain.reply.Reply + +data class NotificationEvent( + val userId: Long, + val postId: Long, + val commentId: Long, + val replyId: Long?, + val content: String, + val community: String, + val type: NotificationType, +) { + companion object { + fun commentToPostUser(post: Post, comment: Comment, community: Community) = + NotificationEvent( + post.userId, + post.id, + comment.id, + null, + comment.content, + community.title, + COMMENT + ) + + fun replyToPostUser(post: Post, comment: Comment, reply: Reply, community: Community) = + NotificationEvent( + post.userId, + post.id, + comment.id, + reply.id, + reply.content, + community.title, + REPLY + ) + + fun replyToCommentUser(post: Post, comment: Comment, reply: Reply, community: Community) = + NotificationEvent( + comment.userId, + post.id, + comment.id, + reply.id, + reply.content, + community.title, + REPLY + ) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/notification/NotificationListener.kt b/backend/src/main/kotlin/com/dclass/backend/domain/notification/NotificationListener.kt new file mode 100644 index 0000000000..c894fc232a --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/notification/NotificationListener.kt @@ -0,0 +1,45 @@ +package com.dclass.backend.domain.notification + +import com.dclass.backend.application.NotificationService +import com.dclass.backend.application.dto.NotificationCommentRequest +import com.dclass.backend.application.dto.NotificationReplyRequest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.springframework.stereotype.Component +import org.springframework.transaction.event.TransactionalEventListener + +@Component +class NotificationListener( + private val notificationService: NotificationService +) { + + @TransactionalEventListener + fun sendNotificationForComment(event: NotificationEvent) = + CoroutineScope(Dispatchers.IO).launch { + if (event.type == NotificationType.COMMENT) { + notificationService.send( + NotificationCommentRequest( + userId = event.userId, + postId = event.postId, + commentId = event.commentId, + content = event.content, + communityTitle = event.community, + type = event.type + ) + ) + } else { + notificationService.send( + NotificationReplyRequest( + userId = event.userId, + postId = event.postId, + commentId = event.commentId, + replyId = event.replyId!!, + content = event.content, + communityTitle = event.community, + type = event.type + ) + ) + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/notification/NotificationRepository.kt b/backend/src/main/kotlin/com/dclass/backend/domain/notification/NotificationRepository.kt new file mode 100644 index 0000000000..bab299bda4 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/notification/NotificationRepository.kt @@ -0,0 +1,11 @@ +package com.dclass.backend.domain.notification + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.repository.findByIdOrNull + +fun NotificationRepository.getOrThrow(id: Long): Notification = findByIdOrNull(id) + ?: throw IllegalArgumentException("Notification not found") + +interface NotificationRepository : JpaRepository { + fun findAllByUserId(userId: Long): List +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/notification/NotificationType.kt b/backend/src/main/kotlin/com/dclass/backend/domain/notification/NotificationType.kt new file mode 100644 index 0000000000..84cde1479a --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/notification/NotificationType.kt @@ -0,0 +1,6 @@ +package com.dclass.backend.domain.notification + +enum class NotificationType(val value: String) { + COMMENT("comment"), REPLY("reply") + +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/post/Post.kt b/backend/src/main/kotlin/com/dclass/backend/domain/post/Post.kt new file mode 100644 index 0000000000..e086a765f5 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/post/Post.kt @@ -0,0 +1,124 @@ +package com.dclass.backend.domain.post + +import com.dclass.backend.exception.post.PostException +import com.dclass.backend.exception.post.PostExceptionType +import com.dclass.support.domain.BaseEntity +import com.dclass.support.domain.Image +import jakarta.persistence.* +import org.hibernate.annotations.SQLDelete +import org.hibernate.annotations.SQLRestriction +import java.time.LocalDateTime + +@SQLDelete(sql = "update post set deleted = true where id = ? and version = ?") +@SQLRestriction("deleted = false") +@Entity +@Table +class Post( + @Column(nullable = false) + val userId: Long, + + @Column(nullable = false) + var communityId: Long, + + title: String = "", + + content: String = "", + + postLikes: PostLikes = PostLikes(), + + images: List = emptyList(), + + @Embedded + var postCount: PostCount = PostCount(), + + isQuestion: Boolean = false, + + @Column(nullable = false) + val createdDateTime: LocalDateTime = LocalDateTime.now(), + + @Version + val version : Long =0L, + + modifiedDateTime: LocalDateTime = LocalDateTime.now(), + + id: Long = 0L, +) : BaseEntity(id) { + + @Column(nullable = false) + private var deleted: Boolean = false + + @Column(nullable = false, length = 100) + var title: String = title + private set + + @Column(nullable = false, length = 255) + var content: String = content + private set + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "post_images") + private val _images: MutableList = images.toMutableList() + + @Embedded + var postLikes: PostLikes = postLikes + private set + + val postLikesCount: Int + get() = postLikes.count + + val images: List + get() = _images + + @Column(nullable = false) + var isQuestion: Boolean = isQuestion + private set + + @Column(nullable = false) + var modifiedDateTime: LocalDateTime = modifiedDateTime + private set + + val thumbnail: String? + get() = _images.firstOrNull()?.imageKey + + fun update(title: String, content: String, images: List, communityId: Long) { + this.title = title + this.content = content + this._images.clear() + this._images.addAll(images) + this.communityId = communityId + this.modifiedDateTime = LocalDateTime.now() + } + + fun increaseCommentReplyCount() { + postCount = postCount.increaseCommentReplyCount() + } + + fun increaseScrapCount() { + postCount = postCount.increaseScrapCount() + } + + fun decreaseScrapCount() { + postCount = postCount.decreaseScrapCount() + } + + fun decreaseCommentReplyCount() { + postCount = postCount.decreaseCommentReplyCount() + } + + fun addLike(userId: Long) { + if (this.userId == userId) { + throw PostException(PostExceptionType.SELF_LIKE) + } + postLikes.add(userId) + postCount = postCount.syncLikeCount(postLikesCount) + } + + fun isEligibleForSSE(userid: Long): Boolean { + return this.userId != userid + } + + fun likedBy(userId: Long): Boolean { + return postLikes.likedBy(userId) + } + +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/post/PostCount.kt b/backend/src/main/kotlin/com/dclass/backend/domain/post/PostCount.kt new file mode 100644 index 0000000000..c4f62599a4 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/post/PostCount.kt @@ -0,0 +1,25 @@ +package com.dclass.backend.domain.post + +import jakarta.persistence.Embeddable + +@Embeddable +class PostCount( + val commentReplyCount: Int = 0, + val likeCount: Int = 0, + val scrapCount: Int = 0, +) { + fun increaseCommentReplyCount() = + PostCount(commentReplyCount + 1, likeCount, scrapCount) + + fun decreaseCommentReplyCount() = + PostCount(commentReplyCount - 1, likeCount, scrapCount) + + fun syncLikeCount(cnt: Int) = + PostCount(commentReplyCount, cnt, scrapCount) + + fun increaseScrapCount() = + PostCount(commentReplyCount, likeCount, scrapCount + 1) + + fun decreaseScrapCount() = + PostCount(commentReplyCount, likeCount, scrapCount - 1) +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/post/PostLike.kt b/backend/src/main/kotlin/com/dclass/backend/domain/post/PostLike.kt new file mode 100644 index 0000000000..297557e4f1 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/post/PostLike.kt @@ -0,0 +1,10 @@ +package com.dclass.backend.domain.post + +import jakarta.persistence.Column +import jakarta.persistence.Embeddable + +@Embeddable +class PostLike( + @Column(nullable = false) + val usersId: Long, +) \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/post/PostLikes.kt b/backend/src/main/kotlin/com/dclass/backend/domain/post/PostLikes.kt new file mode 100644 index 0000000000..278274f6ad --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/post/PostLikes.kt @@ -0,0 +1,30 @@ +package com.dclass.backend.domain.post + +import jakarta.persistence.CollectionTable +import jakarta.persistence.ElementCollection +import jakarta.persistence.Embeddable +import jakarta.persistence.FetchType + +@Embeddable +class PostLikes( + likes: List = emptyList() +) { + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "post_likes") + private val _likes: MutableList = likes.toMutableList() + + val likes: List + get() = _likes + + val count: Int + get() = _likes.size + + fun add(userId: Long) { + _likes.removeIf { it.usersId == userId } + _likes.add(PostLike(userId)) + } + + fun likedBy(userId: Long): Boolean { + return _likes.any { it.usersId == userId } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/post/PostRepository.kt b/backend/src/main/kotlin/com/dclass/backend/domain/post/PostRepository.kt new file mode 100644 index 0000000000..cd83ac6525 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/post/PostRepository.kt @@ -0,0 +1,234 @@ +package com.dclass.backend.domain.post + +import com.dclass.backend.application.dto.PostDetailResponse +import com.dclass.backend.application.dto.PostResponse +import com.dclass.backend.application.dto.PostScrollPageRequest +import com.dclass.backend.domain.comment.Comment +import com.dclass.backend.domain.community.Community +import com.dclass.backend.domain.reply.Reply +import com.dclass.backend.domain.scrap.Scrap +import com.dclass.backend.domain.user.User +import com.dclass.backend.exception.post.PostException +import com.dclass.backend.exception.post.PostExceptionType.NOT_FOUND_POST +import com.linecorp.kotlinjdsl.dsl.jpql.Jpql +import com.linecorp.kotlinjdsl.dsl.jpql.jpql +import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.Predicatable +import com.linecorp.kotlinjdsl.render.jpql.JpqlRenderContext +import com.linecorp.kotlinjdsl.support.spring.data.jpa.extension.createQuery +import jakarta.persistence.EntityManager +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.repository.findByIdOrNull +import java.time.LocalDateTime + +fun PostRepository.findByIdOrThrow(id: Long): Post { + return findByIdOrNull(id) ?: throw PostException(NOT_FOUND_POST) +} + +interface PostRepository : JpaRepository, PostRepositorySupport { + fun findByIdAndUserId(postId: Long, userId: Long): Post? + fun findFirstByUserIdOrderByCreatedDateTimeDesc(userId: Long): Post? +} + +interface PostRepositorySupport { + fun findPostScrollPage(request: PostScrollPageRequest): List + fun findPostById(id: Long): PostDetailResponse + fun findScrapPostByUserId(userId: Long): List + fun findFirstCreatedDateTimeByUserId(userId: Long): LocalDateTime? + + fun findPostScrollPage( + communityIds: List, + request: PostScrollPageRequest + ): List + + fun findPostScrollPageByUserId( + userId: Long, + request: PostScrollPageRequest + ): List + + fun findCommentedAndRepliedPostByUserId( + userId: Long, + request: PostScrollPageRequest + ): List +} + +private class PostRepositoryImpl( + private val em: EntityManager, + private val context: JpqlRenderContext +) : PostRepositorySupport { + override fun findPostScrollPage( + request: PostScrollPageRequest + ): List { + val query = jpql { + select( + entity(Post::class) + ).from( + entity(Post::class) + ).where( + path(Post::id).lessThan(request.lastId ?: Long.MAX_VALUE) + ).orderBy( + path(Post::id).desc() + ) + } + + return em.createQuery(query, context).setMaxResults(request.size).resultList + } + + override fun findPostById(id: Long): PostDetailResponse { + val query = jpql { + selectNew( + entity(Post::class), + entity(User::class), + path(Community::title) + ).from( + entity(Post::class), + join(User::class).on(path(Post::userId).equal(path(User::id))), + join(Community::class).on(path(Post::communityId).equal(path(Community::id))) + ).where( + path(Post::id).equal(id) + ) + } + + return em.createQuery(query, context).singleResult + } + + override fun findPostScrollPage( + communityIds: List, + request: PostScrollPageRequest + ): List { + + val query = jpql { + selectNew( + entity(Post::class), + entity(User::class), + path(Community::title) + ).from( + entity(Post::class), + join(Community::class).on(path(Post::communityId).equal(path(Community::id))), + join(User::class).on(path(Post::userId).equal(path(User::id))) + ).whereAnd( + path(Post::id).lessThan(request.lastId ?: Long.MAX_VALUE), + path(Post::communityId).`in`(communityIds), + request.communityTitle?.let { path(Community::title).equal(it) }, + isHot(request), + searchOption(request) + ).orderBy( + path(Post::id).desc() + ) + } + return em.createQuery(query, context).setMaxResults(request.size).resultList + } + + override fun findPostScrollPageByUserId( + userId: Long, + request: PostScrollPageRequest + ): List { + + val query = jpql { + selectNew( + entity(Post::class), + entity(User::class), + path(Community::title) + ).from( + entity(Post::class), + join(Community::class).on(path(Post::communityId).equal(path(Community::id))), + join(User::class).on(path(Post::userId).equal(path(User::id))) + ).whereAnd( + path(Post::id).lessThan(request.lastId ?: Long.MAX_VALUE), + path(Post::userId).equal(userId), + ).orderBy( + path(Post::id).desc() + ) + } + return em.createQuery(query, context).setMaxResults(request.size).resultList + + } + + override fun findScrapPostByUserId(userId: Long): List { + val query = jpql { + selectNew( + entity(Post::class), + entity(User::class), + path(Community::title) + ).from( + entity(Post::class), + join(Community::class).on(path(Post::communityId).equal(path(Community::id))), + join(User::class).on(path(Post::userId).equal(path(User::id))), + join(Scrap::class).on(path(Post::id).equal(path(Scrap::postId))) + ).whereAnd( + path(Scrap::userId).equal(userId) + ).orderBy( + path(Post::id).desc() + ) + } + return em.createQuery(query, context).resultList + } + + override fun findCommentedAndRepliedPostByUserId( + userId: Long, + request: PostScrollPageRequest + ): List { + val query = jpql { + + val subquery = select( + path(Comment::postId) + ).from( + entity(Comment::class), + join(Reply::class).on(path(Comment::id).equal(path(Reply::commentId))) + ).where( + path(Reply::userId).equal(userId), + ).asSubquery() + + val subquery2 = select( + path(Comment::postId) + ).from( + entity(Comment::class) + ).where( + path(Comment::userId).equal(userId) + ).asSubquery() + + selectNew( + entity(Post::class), + entity(User::class), + path(Community::title) + ).from( + entity(Post::class), + join(Community::class).on(path(Post::communityId).equal(path(Community::id))), + join(User::class).on(path(Post::userId).equal(path(User::id))) + ).whereAnd( + path(Post::id).`in`(subquery).or(path(Post::id).`in`(subquery2)), + path(Post::id).lessThan(request.lastId ?: Long.MAX_VALUE), + ).orderBy( + path(Post::id).desc() + ) + } + return em.createQuery(query, context).setMaxResults(request.size).resultList + } + + override fun findFirstCreatedDateTimeByUserId(userId: Long): LocalDateTime? { + val query = jpql { + select( + path(Post::createdDateTime) + ).from( + entity(Post::class) + ).where( + path(Post::userId).equal(userId) + ).orderBy( + path(Post::createdDateTime).desc() + ) + } + + return em.createQuery(query, context).resultList.firstOrNull() + } + + private fun Jpql.searchOption(request: PostScrollPageRequest): Predicatable? { + return if (request.keyword != null) or( + path(Post::title).like("%${request.keyword}%"), + path(Post::content).like("%${request.keyword}%") + ) else null + } + + private fun Jpql.isHot(request: PostScrollPageRequest): Predicatable? { + return if (request.isHot) path(Post::postCount)(PostCount::likeCount).ge(10) else null + } + +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/reply/Reply.kt b/backend/src/main/kotlin/com/dclass/backend/domain/reply/Reply.kt new file mode 100644 index 0000000000..82fcad37cc --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/reply/Reply.kt @@ -0,0 +1,69 @@ +package com.dclass.backend.domain.reply + +import com.dclass.backend.exception.reply.ReplyException +import com.dclass.backend.exception.reply.ReplyExceptionType.SELF_LIKE +import com.dclass.support.domain.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Embedded +import jakarta.persistence.Entity +import org.hibernate.annotations.SQLDelete +import org.hibernate.annotations.SQLRestriction +import java.time.LocalDateTime + +@SQLDelete(sql = "update reply set deleted = true where id = ?") +@SQLRestriction("deleted = false") +@Entity +class Reply( + @Column(nullable = false) + val userId: Long, + + @Column(nullable = false) + val commentId: Long, + + content: String = "", + + replyLikes: ReplyLikes = ReplyLikes(), + + @Column(nullable = false) + val createdDateTime: LocalDateTime = LocalDateTime.now(), + + modifiedDateTime: LocalDateTime = LocalDateTime.now(), + + id: Long = 0L +) : BaseEntity(id) { + + @Column(nullable = false) + private var deleted: Boolean = false + + @Column(nullable = false, length = 255) + var content: String = content + private set + + @Embedded + var replyLikes: ReplyLikes = replyLikes + private set + + val likeCount: Int + get() = replyLikes.count + + @Column(nullable = false) + var modifiedDateTime: LocalDateTime = modifiedDateTime + private set + + fun changeContent(content: String) { + this.content = content + modifiedDateTime = LocalDateTime.now() + } + + fun isDeleted(replyId: Long) = deleted + + fun likedBy(userId: Long) = + replyLikes.findUserById(userId) + + fun like(userId: Long) { + if (this.userId == userId) { + throw ReplyException(SELF_LIKE) + } + replyLikes.add(userId) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/reply/ReplyLike.kt b/backend/src/main/kotlin/com/dclass/backend/domain/reply/ReplyLike.kt new file mode 100644 index 0000000000..4945905bbe --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/reply/ReplyLike.kt @@ -0,0 +1,10 @@ +package com.dclass.backend.domain.reply + +import jakarta.persistence.Column +import jakarta.persistence.Embeddable + +@Embeddable +class ReplyLike( + @Column(nullable = false) + val usersId: Long, +) \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/reply/ReplyLikes.kt b/backend/src/main/kotlin/com/dclass/backend/domain/reply/ReplyLikes.kt new file mode 100644 index 0000000000..ca7b8950fc --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/reply/ReplyLikes.kt @@ -0,0 +1,30 @@ +package com.dclass.backend.domain.reply + +import jakarta.persistence.CollectionTable +import jakarta.persistence.ElementCollection +import jakarta.persistence.Embeddable +import jakarta.persistence.FetchType + +@Embeddable +class ReplyLikes( + likes: List = emptyList() +) { + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "reply_likes") + private val _likes: MutableList = likes.toMutableList() + + val likes: List + get() = _likes + + val count: Int + get() = _likes.size + + fun add(userId: Long) { + _likes.removeIf { it.usersId == userId } + _likes.add(ReplyLike(userId)) + } + + fun findUserById(userId: Long): Boolean { + return _likes.any { it.usersId == userId } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/reply/ReplyRepository.kt b/backend/src/main/kotlin/com/dclass/backend/domain/reply/ReplyRepository.kt new file mode 100644 index 0000000000..aa00383723 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/reply/ReplyRepository.kt @@ -0,0 +1,18 @@ +package com.dclass.backend.domain.reply + +import com.dclass.backend.exception.reply.ReplyException +import com.dclass.backend.exception.reply.ReplyExceptionType.NOT_FOUND_REPLY +import org.springframework.data.jpa.repository.JpaRepository + +fun ReplyRepository.getByIdOrThrow(id: Long): Reply { + return findById(id).orElseThrow { ReplyException(NOT_FOUND_REPLY) } +} + +fun ReplyRepository.getByIdAndUserIdOrThrow(replyId: Long, userId: Long): Reply { + return findByIdAndUserId(replyId, userId) ?: throw ReplyException(NOT_FOUND_REPLY) +} + +interface ReplyRepository : JpaRepository, ReplyRepositorySupport { + fun findByIdAndUserId(replyId: Long, userId: Long): Reply? + +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/reply/ReplyRepositorySupport.kt b/backend/src/main/kotlin/com/dclass/backend/domain/reply/ReplyRepositorySupport.kt new file mode 100644 index 0000000000..a350aa24f9 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/reply/ReplyRepositorySupport.kt @@ -0,0 +1,40 @@ +package com.dclass.backend.domain.reply + +import com.dclass.backend.application.dto.ReplyWithUserResponse +import com.dclass.backend.domain.user.User +import com.linecorp.kotlinjdsl.dsl.jpql.Jpql +import com.linecorp.kotlinjdsl.dsl.jpql.jpql +import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.Predicatable +import com.linecorp.kotlinjdsl.render.jpql.JpqlRenderContext +import com.linecorp.kotlinjdsl.support.spring.data.jpa.extension.createQuery +import jakarta.persistence.EntityManager + +interface ReplyRepositorySupport { + fun findRepliesWithUserByCommentIdIn(commentIds: List): List +} + +class ReplyRepositoryImpl( + private val em: EntityManager, + private val context: JpqlRenderContext +) : ReplyRepositorySupport { + + override fun findRepliesWithUserByCommentIdIn(commentIds: List): List { + val query = jpql { + selectNew( + entity(Reply::class), + entity(User::class) + ).from( + entity(Reply::class), + join(User::class).on(path(Reply::userId).eq(path(User::id))) + ).where( + replyExist(commentIds) + ) + } + + return em.createQuery(query, context).resultList + } + + private fun Jpql.replyExist(request: List): Predicatable? { + return if (request.isNotEmpty()) path(Reply::commentId).`in`(request) else null + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/report/Report.kt b/backend/src/main/kotlin/com/dclass/backend/domain/report/Report.kt new file mode 100644 index 0000000000..33c0f24a1d --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/report/Report.kt @@ -0,0 +1,32 @@ +package com.dclass.backend.domain.report + +import com.dclass.support.domain.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EnumType.STRING +import jakarta.persistence.Enumerated +import jakarta.persistence.Table +import java.time.LocalDateTime + +@Entity +@Table +class Report( + @Column(nullable = false) + val reporterId: Long, + + @Column(nullable = false) + val reportedObjectId: Long, + + @Column(nullable = false) + @Enumerated(value = STRING) + val reportType: ReportType, + + @Column(nullable = false) + @Enumerated(value = STRING) + val reason: ReportReason, + + @Column(nullable = false) + val createdDateTime: LocalDateTime = LocalDateTime.now(), + + id: Long = 0L +) : BaseEntity(id) \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/report/ReportEvent.kt b/backend/src/main/kotlin/com/dclass/backend/domain/report/ReportEvent.kt new file mode 100644 index 0000000000..0b2766b0d2 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/report/ReportEvent.kt @@ -0,0 +1,26 @@ +package com.dclass.backend.domain.report + +interface ReportEvent { + companion object { + fun create(id: Long, type: ReportType): ReportEvent { + return when (type) { + ReportType.POST -> PostReportedEvent(id) + ReportType.COMMENT -> CommentReportedEvent(id) + ReportType.REPLY -> ReplyReportedEvent(id) + } + } + } +} + +class PostReportedEvent( + val postId: Long +) : ReportEvent + +class CommentReportedEvent( + val commentId: Long +) : ReportEvent + +class ReplyReportedEvent( + val replyId: Long +) : ReportEvent + diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/report/ReportReason.kt b/backend/src/main/kotlin/com/dclass/backend/domain/report/ReportReason.kt new file mode 100644 index 0000000000..b47e7f937f --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/report/ReportReason.kt @@ -0,0 +1,10 @@ +package com.dclass.backend.domain.report + +enum class ReportReason(val value: String) { + INSULTING("욕설/비하"), + COMMERCIAL("상업적 광고 및 판매"), + INAPPROPRIATE("게시판 성격에 부적절함"), + FRAUD("유출/사칭/사기"), + SPAM("낚시/놀람/도배"), + PORNOGRAPHIC("음란물/불건전한 만남 및 대화"), +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/report/ReportRepository.kt b/backend/src/main/kotlin/com/dclass/backend/domain/report/ReportRepository.kt new file mode 100644 index 0000000000..72be5db831 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/report/ReportRepository.kt @@ -0,0 +1,8 @@ +package com.dclass.backend.domain.report + +import org.springframework.data.jpa.repository.JpaRepository + + +interface ReportRepository : JpaRepository, ReportRepositorySupport { + fun findByReporterIdAndAndReportedObjectId(userId: Long, reportedObjectId: Long): Report? +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/report/ReportRepositorySupport.kt b/backend/src/main/kotlin/com/dclass/backend/domain/report/ReportRepositorySupport.kt new file mode 100644 index 0000000000..a8deaeffd7 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/report/ReportRepositorySupport.kt @@ -0,0 +1,30 @@ +package com.dclass.backend.domain.report + +import com.linecorp.kotlinjdsl.dsl.jpql.jpql +import com.linecorp.kotlinjdsl.render.jpql.JpqlRenderContext +import com.linecorp.kotlinjdsl.support.spring.data.jpa.extension.createQuery +import jakarta.persistence.EntityManager + +interface ReportRepositorySupport { + fun countReportById(objectId: Long, type: ReportType): Long +} + +class ReportRepositoryImpl( + private val em: EntityManager, + private val context: JpqlRenderContext +) : ReportRepositorySupport { + override fun countReportById(objectId: Long, type: ReportType): Long { + val query = jpql { + select( + count(entity(Report::class)) + ).from( + entity(Report::class) + ).whereAnd( + path(Report::reportedObjectId).eq(objectId), + path(Report::reportType).eq(type) + ) + } + + return em.createQuery(query, context).singleResult + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/report/ReportType.kt b/backend/src/main/kotlin/com/dclass/backend/domain/report/ReportType.kt new file mode 100644 index 0000000000..ecc3598be3 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/report/ReportType.kt @@ -0,0 +1,5 @@ +package com.dclass.backend.domain.report + +enum class ReportType { + POST, COMMENT, REPLY +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/scrap/Scrap.kt b/backend/src/main/kotlin/com/dclass/backend/domain/scrap/Scrap.kt new file mode 100644 index 0000000000..4eecfd99de --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/scrap/Scrap.kt @@ -0,0 +1,25 @@ +package com.dclass.backend.domain.scrap + +import com.dclass.support.domain.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Table +import org.hibernate.annotations.SQLDelete +import org.hibernate.annotations.SQLRestriction + +@SQLDelete(sql = "update scrap set deleted = true where id = ?") +@SQLRestriction("deleted = false") +@Entity +@Table +class Scrap( + @Column(nullable = false) + val userId: Long, + + @Column(nullable = false) + val postId: Long, + id: Long = 0L, +) : BaseEntity(id) { + + @Column(nullable = false) + private var deleted: Boolean = false +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/scrap/ScrapRepository.kt b/backend/src/main/kotlin/com/dclass/backend/domain/scrap/ScrapRepository.kt new file mode 100644 index 0000000000..9b4be7cae3 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/scrap/ScrapRepository.kt @@ -0,0 +1,8 @@ +package com.dclass.backend.domain.scrap + +import org.springframework.data.jpa.repository.JpaRepository + +interface ScrapRepository : JpaRepository { + fun existsByUserIdAndPostId(userId: Long, postId: Long): Boolean + fun findByUserIdAndPostId(userId: Long, postId: Long): Scrap? +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/user/Password.kt b/backend/src/main/kotlin/com/dclass/backend/domain/user/Password.kt new file mode 100644 index 0000000000..d41e2761a9 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/user/Password.kt @@ -0,0 +1,33 @@ +package com.dclass.backend.domain.user + +import com.dclass.support.security.sha256Encrypt +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import jakarta.persistence.Embeddable + +private class PasswordDeserializer : JsonDeserializer() { + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Password = Password(p.text) +} + +private class PasswordSerializer : JsonSerializer() { + override fun serialize(password: Password, gen: JsonGenerator, serializers: SerializerProvider) { + gen.writeString(password.value) + } +} + +@JsonSerialize(using = PasswordSerializer::class) +@JsonDeserialize(using = PasswordDeserializer::class) +@Embeddable +data class Password( + var value: String +) { + init { + value = sha256Encrypt(value) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/user/PasswordResetEvent.kt b/backend/src/main/kotlin/com/dclass/backend/domain/user/PasswordResetEvent.kt new file mode 100644 index 0000000000..5599e30bcf --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/user/PasswordResetEvent.kt @@ -0,0 +1,11 @@ +package com.dclass.backend.domain.user + +import java.time.LocalDateTime + +class PasswordResetEvent ( + val userId: Long, + val name: String, + val email: String, + val password: String, + val occurredOn: LocalDateTime = LocalDateTime.now() +) \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/user/University.kt b/backend/src/main/kotlin/com/dclass/backend/domain/user/University.kt new file mode 100644 index 0000000000..5ab09583b7 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/user/University.kt @@ -0,0 +1,23 @@ +package com.dclass.backend.domain.user + +import com.dclass.support.domain.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity + +@Entity +class University( + + @Column(nullable = false) + val name: String, + + @Column(nullable = false) + val emailSuffix: String, + + /** + * 학교 로고 이미지 URL을 추가한다 + */ + @Column(nullable = false) + val logo: String = "", + + id: Long = 0L +) : BaseEntity(id) \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/user/UniversityRepository.kt b/backend/src/main/kotlin/com/dclass/backend/domain/user/UniversityRepository.kt new file mode 100644 index 0000000000..3fcc6a4843 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/user/UniversityRepository.kt @@ -0,0 +1,9 @@ +package com.dclass.backend.domain.user + +import org.springframework.data.jpa.repository.JpaRepository + +interface UniversityRepository: JpaRepository { + fun findByName(name: String): University + fun existsByEmailSuffix(emailSuffix: String): Boolean + fun findByEmailSuffix(emailSuffix: String): University +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/user/User.kt b/backend/src/main/kotlin/com/dclass/backend/domain/user/User.kt new file mode 100644 index 0000000000..dac06400d3 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/user/User.kt @@ -0,0 +1,88 @@ +package com.dclass.backend.domain.user + +import com.dclass.backend.exception.user.UserException +import com.dclass.backend.exception.user.UserExceptionType.* +import com.dclass.support.domain.BaseRootEntity +import com.dclass.support.security.sha256Encrypt +import jakarta.persistence.* +import java.time.LocalDateTime + +@Entity +@Table(name = "users") +class User( + + @Embedded + private var information: UserInformation, + + @AttributeOverride(name = "value", column = Column(name = "password", nullable = false)) + @Embedded + var password: Password, + + @ManyToOne(fetch = FetchType.EAGER, optional = false) + @JoinColumn(name = "university_id", nullable = false) + val university: University, + + id: Long = 0L +) : BaseRootEntity(id) { + + @Column(nullable = false) + private var deleted: Boolean = false + + val name: String + get() = information.name + + val email: String + get() = information.email + + val nickname: String + get() = information.nickname + + val universityName: String + get() = university.name + + constructor( + name: String, + email: String, + nickname: String, + password: String, + university: University, + id: Long = 0L + ) : this( + UserInformation(name, email, nickname), Password(password), university, id, + ) + + fun authenticate(password: Password) { + if (password != this.password) throw UserException(INVALID_PASSWORD_ACCESS_DENIED) + } + + fun resetPassword(name: String, password: String) { + if (!information.same(name)) throw UserException(INVALID_USER_INFORMATION) + this.password = Password(password) + registerEvent(PasswordResetEvent(id, name, email, password)) + } + + fun changePassword(oldPassword: Password, newPassword: Password) { + if (oldPassword != password) throw UserException(INVALID_PASSWORD_ACCESS_DENIED) + this.password = newPassword + } + + fun changeNickname(nickname: String) { + this.information = information.copy(nickname = nickname) + } + + fun anonymize() { + this.information = information.copy(name = "", nickname = "(알 수 없음)") + this.deleted = true + } + + fun anonymizeEmail() { + this.information = information.copy(email = sha256Encrypt(LocalDateTime.now().toString())) + } + + fun isDeleted() = deleted + + fun checkExistingAndDeletedUser() { + if (deleted) throw UserException(RESIGNED_USER) + throw UserException(ALREADY_EXIST_USER) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/user/UserInformation.kt b/backend/src/main/kotlin/com/dclass/backend/domain/user/UserInformation.kt new file mode 100644 index 0000000000..d43ddfc30a --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/user/UserInformation.kt @@ -0,0 +1,20 @@ +package com.dclass.backend.domain.user + +import jakarta.persistence.Column +import jakarta.persistence.Embeddable + +@Embeddable +data class UserInformation( + @Column(nullable = false, length = 30) + val name: String, + + @Column(unique = true, nullable = false) + val email: String, + + @Column(nullable = false, length = 13) + val nickname: String +) { + fun same(name: String): Boolean { + return this.name == name + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/user/UserRepository.kt b/backend/src/main/kotlin/com/dclass/backend/domain/user/UserRepository.kt new file mode 100644 index 0000000000..918dd32baa --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/user/UserRepository.kt @@ -0,0 +1,19 @@ +package com.dclass.backend.domain.user + +import com.dclass.backend.exception.user.UserException +import com.dclass.backend.exception.user.UserExceptionType +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.repository.findByIdOrNull + +fun UserRepository.findByEmail(email: String): User? = findByInformationEmail(email) +fun UserRepository.existsByEmail(email: String): Boolean = existsByInformationEmail(email) +fun UserRepository.getOrThrow(id: Long): User = findByIdOrNull(id) + ?: throw UserException(UserExceptionType.NOT_FOUND_USER) + +fun UserRepository.getByEmailOrThrow(email: String): User = findByInformationEmail(email) + ?: throw UserException(UserExceptionType.NOT_FOUND_USER) + +interface UserRepository : JpaRepository, UserRepositorySupport { + fun findByInformationEmail(email: String): User? + fun existsByInformationEmail(email: String): Boolean +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/user/UserRepositorySupport.kt b/backend/src/main/kotlin/com/dclass/backend/domain/user/UserRepositorySupport.kt new file mode 100644 index 0000000000..452b0ba7e0 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/user/UserRepositorySupport.kt @@ -0,0 +1,35 @@ +package com.dclass.backend.domain.user + +import com.dclass.backend.application.dto.UserResponseWithDepartment +import com.dclass.backend.domain.belong.Belong +import com.linecorp.kotlinjdsl.dsl.jpql.jpql +import com.linecorp.kotlinjdsl.render.jpql.JpqlRenderContext +import com.linecorp.kotlinjdsl.support.spring.data.jpa.extension.createQuery +import jakarta.persistence.EntityManager + +// TODO: 제거 결정 후 삭제 + +interface UserRepositorySupport { + fun findUserInfoWithDepartment(id: Long): UserResponseWithDepartment +} + +class UserRepositoryImpl( + private val em: EntityManager, + private val context: JpqlRenderContext, +) : UserRepositorySupport { + override fun findUserInfoWithDepartment(id: Long): UserResponseWithDepartment { + val query = jpql { + selectNew( + entity(User::class), + entity(Belong::class), + ).from( + entity(User::class), + join(Belong::class).on(path(Belong::userId).eq(path(User::id))), + ).where( + path(Belong::userId).eq(id) + ) + } + + return em.createQuery(query, context).singleResult + } +} diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/userblock/UserBlock.kt b/backend/src/main/kotlin/com/dclass/backend/domain/userblock/UserBlock.kt new file mode 100644 index 0000000000..47b3d4e2a9 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/userblock/UserBlock.kt @@ -0,0 +1,18 @@ +package com.dclass.backend.domain.userblock + +import com.dclass.support.domain.BaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Table + +@Entity +@Table +class UserBlock( + @Column(nullable = false) + val blockerUserId: Long, + + @Column(nullable = false) + val blockedUserId: Long, + + id: Long = 0L +) : BaseEntity(id) \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/domain/userblock/UserBlockRepository.kt b/backend/src/main/kotlin/com/dclass/backend/domain/userblock/UserBlockRepository.kt new file mode 100644 index 0000000000..8f1c4d18d1 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/domain/userblock/UserBlockRepository.kt @@ -0,0 +1,7 @@ +package com.dclass.backend.domain.userblock + +import org.springframework.data.jpa.repository.JpaRepository + +interface UserBlockRepository : JpaRepository { + fun findByBlockerUserId(blockerUserId: Long): List +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/authenticationcode/AuthenticationCodeException.kt b/backend/src/main/kotlin/com/dclass/backend/exception/authenticationcode/AuthenticationCodeException.kt new file mode 100644 index 0000000000..036e3ab551 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/authenticationcode/AuthenticationCodeException.kt @@ -0,0 +1,11 @@ +package com.dclass.backend.exception.authenticationcode + +import com.dclass.backend.exception.common.BaseException + +class AuthenticationCodeException( + private val exceptionType: AuthenticationCodeExceptionType +) : BaseException() { + override fun exceptionType(): AuthenticationCodeExceptionType { + return exceptionType + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/authenticationcode/AuthenticationCodeExceptionType.kt b/backend/src/main/kotlin/com/dclass/backend/exception/authenticationcode/AuthenticationCodeExceptionType.kt new file mode 100644 index 0000000000..f1ff23027e --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/authenticationcode/AuthenticationCodeExceptionType.kt @@ -0,0 +1,29 @@ +package com.dclass.backend.exception.authenticationcode + +import com.dclass.backend.exception.common.BaseExceptionType +import org.springframework.http.HttpStatus + +enum class AuthenticationCodeExceptionType( + private val httpStatus: HttpStatus, + private val code: String, + private val errorMessage: String +) : BaseExceptionType { + + NOT_EQUAL_CODE(HttpStatus.BAD_REQUEST, "A01", "인증 코드가 일치하지 않습니다"), + ALREADY_VERIFIED(HttpStatus.BAD_REQUEST, "A02", "이미 인증된 코드입니다"), + NOT_VERIFIED(HttpStatus.BAD_REQUEST, "A03", "인증되지 않은 코드입니다"), + EXPIRED_CODE(HttpStatus.BAD_REQUEST, "A04", "만료된 코드입니다"), + ; + + override fun httpStatus(): HttpStatus { + return httpStatus + } + + override fun code(): String { + return code + } + + override fun errorMessage(): String { + return errorMessage + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/belong/BelongException.kt b/backend/src/main/kotlin/com/dclass/backend/exception/belong/BelongException.kt new file mode 100644 index 0000000000..33aadb7e5b --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/belong/BelongException.kt @@ -0,0 +1,11 @@ +package com.dclass.backend.exception.belong + +import com.dclass.backend.exception.common.BaseException + +class BelongException( + private val exceptionType: BelongExceptionType +) : BaseException() { + override fun exceptionType(): BelongExceptionType { + return exceptionType + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/belong/BelongExceptionType.kt b/backend/src/main/kotlin/com/dclass/backend/exception/belong/BelongExceptionType.kt new file mode 100644 index 0000000000..b12d10b324 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/belong/BelongExceptionType.kt @@ -0,0 +1,28 @@ +package com.dclass.backend.exception.belong + +import com.dclass.backend.exception.common.BaseExceptionType +import org.springframework.http.HttpStatus + +enum class BelongExceptionType( + private val httpStatus: HttpStatus, + private val code: String, + private val errorMessage: String +) : BaseExceptionType { + + NOT_FOUND_BELONG(HttpStatus.NOT_FOUND, "B01", "해당 소속을 찾을 수 없습니다."), + MAX_BELONG(HttpStatus.BAD_REQUEST, "B02", "소속은 최대 2개까지만 설정할 수 있습니다."), + CHANGE_INTERVAL_VIOLATION(HttpStatus.BAD_REQUEST, "B03", "학과 변경은 90일마다 가능합니다."), + ; + + override fun httpStatus(): HttpStatus { + return httpStatus + } + + override fun code(): String { + return code + } + + override fun errorMessage(): String { + return errorMessage + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/blacklist/BlacklistException.kt b/backend/src/main/kotlin/com/dclass/backend/exception/blacklist/BlacklistException.kt new file mode 100644 index 0000000000..25b5492758 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/blacklist/BlacklistException.kt @@ -0,0 +1,11 @@ +package com.dclass.backend.exception.blacklist + +import com.dclass.backend.exception.common.BaseException + +class BlacklistException( + private val exceptionType: BlacklistExceptionType +) : BaseException() { + override fun exceptionType(): BlacklistExceptionType { + return exceptionType + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/blacklist/BlacklistExceptionType.kt b/backend/src/main/kotlin/com/dclass/backend/exception/blacklist/BlacklistExceptionType.kt new file mode 100644 index 0000000000..7dab8b8c6d --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/blacklist/BlacklistExceptionType.kt @@ -0,0 +1,26 @@ +package com.dclass.backend.exception.blacklist + +import com.dclass.backend.exception.common.BaseExceptionType +import org.springframework.http.HttpStatus + +enum class BlacklistExceptionType( + private val httpStatus: HttpStatus, + private val code: String, + private val errorMessage: String +) : BaseExceptionType { + + ALREADY_LOGOUT(HttpStatus.UNAUTHORIZED, "B01", "이미 무효화된 토큰입니다. 다시 로그인 해주세요."), + ; + + override fun httpStatus(): HttpStatus { + return httpStatus + } + + override fun code(): String { + return code + } + + override fun errorMessage(): String { + return errorMessage + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/blocklist/BlocklistException.kt b/backend/src/main/kotlin/com/dclass/backend/exception/blocklist/BlocklistException.kt new file mode 100644 index 0000000000..be22cb6cd2 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/blocklist/BlocklistException.kt @@ -0,0 +1,11 @@ +package com.dclass.backend.exception.blocklist + +import com.dclass.backend.exception.common.BaseException + +class BlocklistException( + private val exceptionType: BlocklistExceptionType +) : BaseException() { + override fun exceptionType(): BlocklistExceptionType { + return exceptionType + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/blocklist/BlocklistExceptionType.kt b/backend/src/main/kotlin/com/dclass/backend/exception/blocklist/BlocklistExceptionType.kt new file mode 100644 index 0000000000..bd4de866cd --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/blocklist/BlocklistExceptionType.kt @@ -0,0 +1,26 @@ +package com.dclass.backend.exception.blocklist + +import com.dclass.backend.exception.common.BaseExceptionType +import org.springframework.http.HttpStatus + +enum class BlocklistExceptionType( + private val httpStatus: HttpStatus, + private val code: String, + private val errorMessage: String +) : BaseExceptionType { + NOT_FOUND_USER(HttpStatus.NOT_FOUND, "B01", "정지된 사용자가 아닙니다."), + BLOCKED_USER(HttpStatus.FORBIDDEN, "B02", "정지된 사용자입니다.") + ; + + override fun httpStatus(): HttpStatus { + return httpStatus + } + + override fun code(): String { + return code + } + + override fun errorMessage(): String { + return errorMessage + } +} diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/comment/CommentException.kt b/backend/src/main/kotlin/com/dclass/backend/exception/comment/CommentException.kt new file mode 100644 index 0000000000..1f0e7caf27 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/comment/CommentException.kt @@ -0,0 +1,11 @@ +package com.dclass.backend.exception.comment + +import com.dclass.backend.exception.common.BaseException + +class CommentException( + private val exceptionType: CommentExceptionType +) : BaseException() { + override fun exceptionType(): CommentExceptionType { + return exceptionType + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/comment/CommentExceptionType.kt b/backend/src/main/kotlin/com/dclass/backend/exception/comment/CommentExceptionType.kt new file mode 100644 index 0000000000..6b6c811d35 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/comment/CommentExceptionType.kt @@ -0,0 +1,29 @@ +package com.dclass.backend.exception.comment + +import com.dclass.backend.exception.common.BaseExceptionType +import org.springframework.http.HttpStatus + +enum class CommentExceptionType( + private val httpStatus: HttpStatus, + private val code: String, + private val errorMessage: String +) : BaseExceptionType { + + FORBIDDEN_COMMENT(HttpStatus.FORBIDDEN, "C00", "댓글에 대한 권한이 없습니다."), + NOT_FOUND_COMMENT(HttpStatus.NOT_FOUND, "C01", "해당 댓글을 찾을 수 없습니다."), + SELF_LIKE(HttpStatus.BAD_REQUEST, "C02", "본인이 작성한 댓글에는 좋아요를 누를 수 없습니다."), + DELETED_COMMENT(HttpStatus.BAD_REQUEST, "C03", "삭제된 댓글 입니다."); + + override fun httpStatus(): HttpStatus { + return httpStatus + } + + override fun code(): String { + return code + } + + override fun errorMessage(): String { + return errorMessage + } +} + diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/common/BaseException.kt b/backend/src/main/kotlin/com/dclass/backend/exception/common/BaseException.kt new file mode 100644 index 0000000000..4ed23a7f8a --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/common/BaseException.kt @@ -0,0 +1,5 @@ +package com.dclass.backend.exception.common + +abstract class BaseException : RuntimeException(){ + abstract fun exceptionType(): BaseExceptionType +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/common/BaseExceptionType.kt b/backend/src/main/kotlin/com/dclass/backend/exception/common/BaseExceptionType.kt new file mode 100644 index 0000000000..edf7413355 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/common/BaseExceptionType.kt @@ -0,0 +1,9 @@ +package com.dclass.backend.exception.common + +import org.springframework.http.HttpStatus + +interface BaseExceptionType { + fun httpStatus(): HttpStatus + fun code(): String + fun errorMessage(): String +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/common/ExceptionControllerAdvice.kt b/backend/src/main/kotlin/com/dclass/backend/exception/common/ExceptionControllerAdvice.kt new file mode 100644 index 0000000000..41cb6f577a --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/common/ExceptionControllerAdvice.kt @@ -0,0 +1,86 @@ +package com.dclass.backend.exception.common + +import com.dclass.support.util.logger +import jakarta.servlet.http.HttpServletRequest +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.MethodArgumentNotValidException +import org.springframework.web.bind.MissingServletRequestParameterException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import java.io.IOException + +@RestControllerAdvice +class ExceptionControllerAdvice { + + private val log = logger() + + @ExceptionHandler(BaseException::class) + fun handleBaseException(request: HttpServletRequest, e: BaseException): ResponseEntity { + val type = e.exceptionType() + log.warn("잘못된 요청이 들어왔습니다. URI: ${request.requestURI}, 코드: ${type.code()}, 내용: ${type.errorMessage()}") + return ResponseEntity.status(type.httpStatus()).body( + ExceptionResponse( + code = type.code(), + message = type.errorMessage() + ) + ) + } + + @ExceptionHandler(MethodArgumentNotValidException::class) + fun handleMethodArgumentNotValid( + request: HttpServletRequest, + e: MethodArgumentNotValidException + ): ResponseEntity { + val globalErrorMessage = e.globalErrors.joinToString( + prefix = "[Global Error : ", + separator = ", ", + postfix = "], \t", + transform = { "${it.defaultMessage}" } + ) + val fieldErrorMessage = e.fieldErrors.joinToString( + prefix = "[Field Error : ", + separator = " ", + postfix = "]", + transform = { "${it.field} : ${it.defaultMessage}" } + ) + val errorMessage = globalErrorMessage + fieldErrorMessage + log.warn("잘못된 요청이 들어왔습니다. URI: ${request.requestURI}, 내용: $errorMessage") + return ResponseEntity.badRequest().body( + ExceptionResponse( + code = "G01", + message = errorMessage + ) + ) + } + + @ExceptionHandler(MissingServletRequestParameterException::class) + fun handleMissingServletRequestParameterException( + request: HttpServletRequest, + e: MissingServletRequestParameterException + ): ResponseEntity { + val errorMessage = "${e.parameterName} 값이 누락되었습니다." + log.warn("잘못된 요청이 들어왔습니다. URI: ${request.requestURI}, 내용: $errorMessage") + return ResponseEntity.badRequest().body( + ExceptionResponse( + code = "G02", + message = errorMessage + ) + ) + } + + @ExceptionHandler(IOException::class) + fun handleClientAbortException(request: HttpServletRequest, e: Exception) { + log.warn("클라이언트가 연결을 끊었습니다. URI: ${request.requestURI}, ${e.message}") + } + + @ExceptionHandler(Exception::class) + fun handleException(request: HttpServletRequest, e: Exception): ResponseEntity { + log.error("예상하지 못한 예외가 발생했습니다. URI: ${request.requestURI}, ${e.message}", e) + return ResponseEntity.internalServerError().body( + ExceptionResponse( + code = "G03", + message = "서버가 응답할 수 없습니다." + ) + ) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/common/ExceptionResponse.kt b/backend/src/main/kotlin/com/dclass/backend/exception/common/ExceptionResponse.kt new file mode 100644 index 0000000000..5fdb4c16b1 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/common/ExceptionResponse.kt @@ -0,0 +1,6 @@ +package com.dclass.backend.exception.common + +data class ExceptionResponse( + val code: String, + val message: String, +) diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/community/CommunityException.kt b/backend/src/main/kotlin/com/dclass/backend/exception/community/CommunityException.kt new file mode 100644 index 0000000000..eec4029a0f --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/community/CommunityException.kt @@ -0,0 +1,11 @@ +package com.dclass.backend.exception.community + +import com.dclass.backend.exception.common.BaseException + +class CommunityException( + private val exceptionType: CommunityExceptionType +) : BaseException() { + override fun exceptionType(): CommunityExceptionType { + return exceptionType + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/community/CommunityExceptionType.kt b/backend/src/main/kotlin/com/dclass/backend/exception/community/CommunityExceptionType.kt new file mode 100644 index 0000000000..e21366706e --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/community/CommunityExceptionType.kt @@ -0,0 +1,26 @@ +package com.dclass.backend.exception.community + +import com.dclass.backend.exception.common.BaseExceptionType +import org.springframework.http.HttpStatus + +enum class CommunityExceptionType( + private val httpStatus: HttpStatus, + private val code: String, + private val errorMessage: String +) : BaseExceptionType { + + NOT_FOUND_COMMUNITY(HttpStatus.NOT_FOUND, "C01", "존재하지 않는 커뮤니티입니다."), + ; + + override fun httpStatus(): HttpStatus { + return httpStatus + } + + override fun code(): String { + return code + } + + override fun errorMessage(): String { + return errorMessage + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/department/DepartmentException.kt b/backend/src/main/kotlin/com/dclass/backend/exception/department/DepartmentException.kt new file mode 100644 index 0000000000..19c0044617 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/department/DepartmentException.kt @@ -0,0 +1,11 @@ +package com.dclass.backend.exception.department + +import com.dclass.backend.exception.common.BaseException + +class DepartmentException( + private val exceptionType: DepartmentExceptionType +) : BaseException() { + override fun exceptionType(): DepartmentExceptionType { + return exceptionType + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/department/DepartmentExceptionType.kt b/backend/src/main/kotlin/com/dclass/backend/exception/department/DepartmentExceptionType.kt new file mode 100644 index 0000000000..5141619b31 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/department/DepartmentExceptionType.kt @@ -0,0 +1,26 @@ +package com.dclass.backend.exception.department + +import com.dclass.backend.exception.common.BaseExceptionType +import org.springframework.http.HttpStatus + +enum class DepartmentExceptionType( + private val httpStatus: HttpStatus, + private val code: String, + private val errorMessage: String +) : BaseExceptionType { + + NOT_FOUND_DEPARTMENT(HttpStatus.NOT_FOUND, "C01", "존재하지 않는 학과입니다."), + ; + + override fun httpStatus(): HttpStatus { + return httpStatus + } + + override fun code(): String { + return code + } + + override fun errorMessage(): String { + return errorMessage + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/post/PostException.kt b/backend/src/main/kotlin/com/dclass/backend/exception/post/PostException.kt new file mode 100644 index 0000000000..4d9120e28a --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/post/PostException.kt @@ -0,0 +1,11 @@ +package com.dclass.backend.exception.post + +import com.dclass.backend.exception.common.BaseException + +class PostException( + private val exceptionType: PostExceptionType +) : BaseException() { + override fun exceptionType(): PostExceptionType { + return exceptionType + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/post/PostExceptionType.kt b/backend/src/main/kotlin/com/dclass/backend/exception/post/PostExceptionType.kt new file mode 100644 index 0000000000..8dc6e0191b --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/post/PostExceptionType.kt @@ -0,0 +1,29 @@ +package com.dclass.backend.exception.post + +import com.dclass.backend.exception.common.BaseExceptionType +import org.springframework.http.HttpStatus + +enum class PostExceptionType( + private val httpStatus: HttpStatus, + private val code: String, + private val errorMessage: String +) : BaseExceptionType { + + NOT_FOUND_POST(HttpStatus.NOT_FOUND, "P01", "존재하지 않는 게시글입니다"), + SELF_LIKE(HttpStatus.BAD_REQUEST, "P02", "본인 게시글에 좋아요를 누를 수 없습니다"), + FORBIDDEN_POST(HttpStatus.FORBIDDEN, "P43", "해당 커뮤니티에 게시글을 작성할 수 없습니다"), + POST_DELAY(HttpStatus.BAD_REQUEST, "P44", "게시글을 너무 자주 작성할 수 없습니다. 잠시 후 다시 시도해주세요"), + ; + + override fun httpStatus(): HttpStatus { + return httpStatus + } + + override fun code(): String { + return code + } + + override fun errorMessage(): String { + return errorMessage + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/reply/ReplyException.kt b/backend/src/main/kotlin/com/dclass/backend/exception/reply/ReplyException.kt new file mode 100644 index 0000000000..8a8f4dc09b --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/reply/ReplyException.kt @@ -0,0 +1,11 @@ +package com.dclass.backend.exception.reply + +import com.dclass.backend.exception.common.BaseException + +class ReplyException( + private val exceptionType: ReplyExceptionType +) : BaseException() { + override fun exceptionType(): ReplyExceptionType { + return exceptionType + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/reply/ReplyExceptionType.kt b/backend/src/main/kotlin/com/dclass/backend/exception/reply/ReplyExceptionType.kt new file mode 100644 index 0000000000..cf53911bd3 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/reply/ReplyExceptionType.kt @@ -0,0 +1,28 @@ +package com.dclass.backend.exception.reply + +import com.dclass.backend.exception.common.BaseExceptionType +import org.springframework.http.HttpStatus + +enum class ReplyExceptionType( + private val httpStatus: HttpStatus, + private val code: String, + private val errorMessage: String +) : BaseExceptionType { + + FORBIDDEN_REPLY(HttpStatus.FORBIDDEN, "R00", "대댓글에 대한 권한이 없습니다."), + NOT_FOUND_REPLY(HttpStatus.NOT_FOUND, "R01", "존재하지 않는 댓글입니다."), + SELF_LIKE(HttpStatus.BAD_REQUEST, "R02", "본인이 작성한 대댓글에는 좋아요를 누를 수 없습니다.") + ; + + override fun httpStatus(): HttpStatus { + return httpStatus + } + + override fun code(): String { + return code + } + + override fun errorMessage(): String { + return errorMessage + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/scrap/ScrapException.kt b/backend/src/main/kotlin/com/dclass/backend/exception/scrap/ScrapException.kt new file mode 100644 index 0000000000..6887c4027b --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/scrap/ScrapException.kt @@ -0,0 +1,11 @@ +package com.dclass.backend.exception.scrap + +import com.dclass.backend.exception.common.BaseException + +class ScrapException( + private val exceptionType: ScrapExceptionType +) : BaseException() { + override fun exceptionType(): ScrapExceptionType { + return exceptionType + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/scrap/ScrapExceptionType.kt b/backend/src/main/kotlin/com/dclass/backend/exception/scrap/ScrapExceptionType.kt new file mode 100644 index 0000000000..ed3a10a4f9 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/scrap/ScrapExceptionType.kt @@ -0,0 +1,28 @@ +package com.dclass.backend.exception.scrap + +import com.dclass.backend.exception.common.BaseExceptionType +import org.springframework.http.HttpStatus + +enum class ScrapExceptionType( + private val httpStatus: HttpStatus, + private val code: String, + private val errorMessage: String +) : BaseExceptionType { + + ALREADY_SCRAP_POST(HttpStatus.FORBIDDEN, "S01", "이미 스크랩한 게시물입니다."), + PERMISSION_DENIED(HttpStatus.FORBIDDEN, "S02", "자신이 속한 학과 게시물만 스크랩 할 수 있습니다."), + NOT_FOUND_SCRAP(HttpStatus.NOT_FOUND, "S03", "스크랩한 게시물이 존재하지 않습니다.") + ; + + override fun httpStatus(): HttpStatus { + return httpStatus + } + + override fun code(): String { + return code + } + + override fun errorMessage(): String { + return errorMessage + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/token/TokenException.kt b/backend/src/main/kotlin/com/dclass/backend/exception/token/TokenException.kt new file mode 100644 index 0000000000..d3576677ba --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/token/TokenException.kt @@ -0,0 +1,11 @@ +package com.dclass.backend.exception.token + +import com.dclass.backend.exception.common.BaseException + +class TokenException( + private val exceptionType: TokenExceptionType +) : BaseException() { + override fun exceptionType(): TokenExceptionType { + return exceptionType + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/token/TokenExceptionType.kt b/backend/src/main/kotlin/com/dclass/backend/exception/token/TokenExceptionType.kt new file mode 100644 index 0000000000..8a8b229f85 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/token/TokenExceptionType.kt @@ -0,0 +1,32 @@ +package com.dclass.backend.exception.token + +import com.dclass.backend.exception.common.BaseExceptionType +import org.springframework.http.HttpStatus + +enum class TokenExceptionType( + private val httpStatus: HttpStatus, + private val code: String, + private val errorMessage: String +) : BaseExceptionType { + + NOT_FOUND_TOKEN(HttpStatus.UNAUTHORIZED, "T01", "토큰이 존재하지 않습니다."), + EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "T02", "토큰이 만료되었습니다"), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "T03", "유효하지 않은 토큰입니다."), + WRONG_TOKEN_TYPE(HttpStatus.UNAUTHORIZED, "T04", "토큰 타입이 잘못되었습니다."), + UNSUPPORTED_TOKEN(HttpStatus.UNAUTHORIZED, "T05", "지원하지 않는 형식의 토큰입니다."), + TOKEN_NOT_FOUND_IN_BLACKLIST(HttpStatus.UNAUTHORIZED, "T06", "로그아웃된 사용자입니다."), + CANNOT_CREATE_TOKEN(HttpStatus.INTERNAL_SERVER_ERROR, "T07", "토큰을 생성할 수 없습니다.") + ; + + override fun httpStatus(): HttpStatus { + return httpStatus + } + + override fun code(): String { + return code + } + + override fun errorMessage(): String { + return errorMessage + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/university/UniversityException.kt b/backend/src/main/kotlin/com/dclass/backend/exception/university/UniversityException.kt new file mode 100644 index 0000000000..70c437061b --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/university/UniversityException.kt @@ -0,0 +1,12 @@ +package com.dclass.backend.exception.university + +import com.dclass.backend.exception.common.BaseException +import com.dclass.backend.exception.common.BaseExceptionType + +class UniversityException( + private val exceptionType: UniversityExceptionType +) : BaseException() { + override fun exceptionType(): BaseExceptionType { + return exceptionType + } +} diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/university/UniversityExceptionType.kt b/backend/src/main/kotlin/com/dclass/backend/exception/university/UniversityExceptionType.kt new file mode 100644 index 0000000000..f4ed020bba --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/university/UniversityExceptionType.kt @@ -0,0 +1,26 @@ +package com.dclass.backend.exception.university + +import com.dclass.backend.exception.common.BaseExceptionType +import org.springframework.http.HttpStatus + +enum class UniversityExceptionType( + private val httpStatus: HttpStatus, + private val code: String, + private val errorMessage: String +) : BaseExceptionType { + + NOT_FOUND_UNIVERSITY(HttpStatus.NOT_FOUND, "U40", "존재하지 않는 대학입니다."), + ; + + override fun httpStatus(): HttpStatus { + return httpStatus + } + + override fun code(): String { + return code + } + + override fun errorMessage(): String { + return errorMessage + } +} diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/user/UserException.kt b/backend/src/main/kotlin/com/dclass/backend/exception/user/UserException.kt new file mode 100644 index 0000000000..477ecb5eb3 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/user/UserException.kt @@ -0,0 +1,12 @@ +package com.dclass.backend.exception.user + +import com.dclass.backend.exception.common.BaseException +import com.dclass.backend.exception.community.CommunityExceptionType + +class UserException( + private val exceptionType: UserExceptionType +) : BaseException() { + override fun exceptionType(): UserExceptionType { + return exceptionType + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/exception/user/UserExceptionType.kt b/backend/src/main/kotlin/com/dclass/backend/exception/user/UserExceptionType.kt new file mode 100644 index 0000000000..0884a0e61b --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/exception/user/UserExceptionType.kt @@ -0,0 +1,31 @@ +package com.dclass.backend.exception.user + +import com.dclass.backend.exception.common.BaseExceptionType +import org.springframework.http.HttpStatus + +enum class UserExceptionType( + private val httpStatus: HttpStatus, + private val code: String, + private val errorMessage: String +) : BaseExceptionType { + + INVALID_PASSWORD_ACCESS_DENIED(HttpStatus.UNAUTHORIZED, "C01", "올바르지 않은 비밀번호 입니다."), + INVALID_USER_INFORMATION(HttpStatus.UNAUTHORIZED, "C02", "사용자 정보가 일치하지 않습니다."), + + NOT_FOUND_USER(HttpStatus.NOT_FOUND, "C40", "존재하지 않는 사용자입니다."), + ALREADY_EXIST_USER(HttpStatus.BAD_REQUEST, "C41", "이미 존재하는 사용자입니다."), + RESIGNED_USER(HttpStatus.BAD_REQUEST, "C42", "탈퇴한 사용자입니다."), + ; + + override fun httpStatus(): HttpStatus { + return httpStatus + } + + override fun code(): String { + return code + } + + override fun errorMessage(): String { + return errorMessage + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/infra/AwsProperties.kt b/backend/src/main/kotlin/com/dclass/backend/infra/AwsProperties.kt new file mode 100644 index 0000000000..ee6ad0e23c --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/infra/AwsProperties.kt @@ -0,0 +1,6 @@ +package com.dclass.backend.infra + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("aws") +data class AwsProperties(val accessKey: String, val secretKey: String) \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/infra/S3Client.kt b/backend/src/main/kotlin/com/dclass/backend/infra/S3Client.kt new file mode 100644 index 0000000000..5eff6a94b6 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/infra/S3Client.kt @@ -0,0 +1,18 @@ +package com.dclass.backend.infra + +import aws.sdk.kotlin.runtime.auth.credentials.StaticCredentialsProvider +import aws.sdk.kotlin.services.s3.S3Client +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class S3Configuration(private val awsProperties: AwsProperties) { + @Bean(destroyMethod = "close") + fun s3Client() = S3Client { + region = "ap-northeast-2" + credentialsProvider = StaticCredentialsProvider { + accessKeyId = awsProperties.accessKey + secretAccessKey = awsProperties.secretKey + } + } +} diff --git a/backend/src/main/kotlin/com/dclass/backend/infra/SesClient.kt b/backend/src/main/kotlin/com/dclass/backend/infra/SesClient.kt new file mode 100644 index 0000000000..cb7f9ae327 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/infra/SesClient.kt @@ -0,0 +1,19 @@ +package com.dclass.backend.infra + +import aws.sdk.kotlin.runtime.auth.credentials.StaticCredentialsProvider +import aws.sdk.kotlin.services.ses.SesClient +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + + +@Configuration +class SesConfiguration(private val awsProperties: AwsProperties) { + @Bean(destroyMethod = "close") + fun sesClient() = SesClient { + region = "ap-northeast-2" + credentialsProvider = StaticCredentialsProvider { + accessKeyId = awsProperties.accessKey + secretAccessKey = awsProperties.secretKey + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/infra/mail/AwsMailSender.kt b/backend/src/main/kotlin/com/dclass/backend/infra/mail/AwsMailSender.kt new file mode 100644 index 0000000000..d28fe25bb3 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/infra/mail/AwsMailSender.kt @@ -0,0 +1,44 @@ +package com.dclass.backend.infra.mail + +import aws.sdk.kotlin.services.ses.SesClient +import aws.sdk.kotlin.services.ses.model.* +import com.dclass.backend.application.mail.MailSender +import org.springframework.stereotype.Component + +@Component +class AwsMailSender( + private val client: SesClient +) : MailSender { + + override suspend fun send(toAddress: String, subjectVal: String, bodyHtml: String) { + val destinationOb = Destination { + toAddresses = listOf(toAddress) + } + + val contentOb = Content { + data = bodyHtml + } + + val subOb = Content { + data = subjectVal + } + + val bodyOb = Body { + html = contentOb + } + + val msgOb = Message { + subject = subOb + body = bodyOb + } + + val emailRequest = SendEmailRequest { + destination = destinationOb + message = msgOb + source = "devbelly@naver.com" + } + + println("Attempting to send an email through Amazon SES using the AWS SDK for Kotlin...") + client.sendEmail(emailRequest) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/infra/s3/AwsPresigner.kt b/backend/src/main/kotlin/com/dclass/backend/infra/s3/AwsPresigner.kt new file mode 100644 index 0000000000..6c96298625 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/infra/s3/AwsPresigner.kt @@ -0,0 +1,41 @@ +package com.dclass.backend.infra.s3 + +import aws.sdk.kotlin.services.s3.S3Client +import aws.sdk.kotlin.services.s3.model.GetObjectRequest +import aws.sdk.kotlin.services.s3.model.PutObjectRequest +import aws.sdk.kotlin.services.s3.presigners.presignGetObject +import aws.sdk.kotlin.services.s3.presigners.presignPutObject +import org.springframework.stereotype.Component +import kotlin.time.Duration.Companion.minutes + +@Component +class AwsPresigner( + private val s3Properties: AwsS3Properties, + private val client: S3Client +) { + companion object { + const val POST_IMAGE_FOLDER = "post" + } + + suspend fun getPostObjectPresigned(keyName: String): String { + val unsignedRequest = GetObjectRequest { + bucket = s3Properties.bucket + key = "$POST_IMAGE_FOLDER/$keyName" + } + + val presignedRequest = client.presignGetObject(unsignedRequest, 10.minutes) + + return presignedRequest.url.toString() + } + + suspend fun putPostObjectPresigned(keyName: String): String { + val unsignedRequest = PutObjectRequest { + bucket = s3Properties.bucket + key = "$POST_IMAGE_FOLDER/$keyName" + } + + val presignedRequest = client.presignPutObject(unsignedRequest, 10.minutes) + + return presignedRequest.url.toString() + } +} diff --git a/backend/src/main/kotlin/com/dclass/backend/infra/s3/AwsS3Properties.kt b/backend/src/main/kotlin/com/dclass/backend/infra/s3/AwsS3Properties.kt new file mode 100644 index 0000000000..f136d215bb --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/infra/s3/AwsS3Properties.kt @@ -0,0 +1,6 @@ +package com.dclass.backend.infra.s3 + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("aws.s3") +data class AwsS3Properties(val bucket: String, val region: String) \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/security/AuthToken.kt b/backend/src/main/kotlin/com/dclass/backend/security/AuthToken.kt new file mode 100644 index 0000000000..7b43388902 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/security/AuthToken.kt @@ -0,0 +1,8 @@ +package com.dclass.backend.security + +import io.swagger.v3.oas.annotations.Hidden + +@Hidden +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +annotation class AuthToken diff --git a/backend/src/main/kotlin/com/dclass/backend/security/AuthTokenResolver.kt b/backend/src/main/kotlin/com/dclass/backend/security/AuthTokenResolver.kt new file mode 100644 index 0000000000..a0c297846f --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/security/AuthTokenResolver.kt @@ -0,0 +1,50 @@ +package com.dclass.backend.security + +import com.dclass.backend.exception.token.TokenException +import com.dclass.backend.exception.token.TokenExceptionType +import org.springframework.core.MethodParameter +import org.springframework.http.HttpHeaders +import org.springframework.stereotype.Component +import org.springframework.web.bind.support.WebDataBinderFactory +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer + +private const val BEARER = "Bearer" + +@Component +class AuthTokenResolver : HandlerMethodArgumentResolver { + + override fun supportsParameter(parameter: MethodParameter): Boolean { + return parameter.hasParameterAnnotation(AuthToken::class.java) + } + + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory? + ): String { + return extractBearerToken(webRequest) + } + + private fun extractBearerToken(request: NativeWebRequest): String { + val authorization = + request.getHeader(HttpHeaders.AUTHORIZATION) + ?: throw TokenException(TokenExceptionType.NOT_FOUND_TOKEN) + val (tokenType, token) = splitToTokenFormat(authorization) + if (tokenType != BEARER) { + throw TokenException(TokenExceptionType.WRONG_TOKEN_TYPE) + } + return token + } + + private fun splitToTokenFormat(authorization: String): Pair { + return try { + val tokenFormat = authorization.split(" ") + tokenFormat[0] to tokenFormat[1] + } catch (e: IndexOutOfBoundsException) { + throw TokenException(TokenExceptionType.UNSUPPORTED_TOKEN) + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/security/JwtTokenProvider.kt b/backend/src/main/kotlin/com/dclass/backend/security/JwtTokenProvider.kt new file mode 100644 index 0000000000..bb01cff437 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/security/JwtTokenProvider.kt @@ -0,0 +1,75 @@ +package com.dclass.backend.security + +import com.dclass.backend.exception.token.TokenException +import com.dclass.backend.exception.token.TokenExceptionType.* +import io.jsonwebtoken.ExpiredJwtException +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.MalformedJwtException +import io.jsonwebtoken.UnsupportedJwtException +import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.security.SignatureException +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import java.util.* +import javax.crypto.SecretKey + +@Component +class JwtTokenProvider ( + @Value("\${jwt.secret}") private val secretKey: String +){ + private var signingKey: SecretKey = secretKey.toByteArray().let { + Keys.hmacShaKeyFor(it) + } + + companion object { + const val ACCESS_TOKEN_EXPIRATION_MILLISECONDS: Long = 1000 * 60 * 60 * 1 // 1시간 + const val REFRESH_TOKEN_EXPIRATION_MILLISECONDS: Long = 1000 * 60 * 60 * 24 * 14 // 2주 + } + + fun createAccessToken(payload: String): String { + return createToken(payload, ACCESS_TOKEN_EXPIRATION_MILLISECONDS) + } + + fun createRefreshToken(payload: String): String { + return createToken(payload, REFRESH_TOKEN_EXPIRATION_MILLISECONDS) + } + + fun createToken(payload: String, validity: Long): String { + return try { + Jwts.builder() + .setSubject(payload) + .setExpiration(Date(System.currentTimeMillis() + validity)) + .signWith(signingKey) + .compact() + } catch (e: IllegalArgumentException) { + throw TokenException(CANNOT_CREATE_TOKEN) + } + } + + fun getSubject(token: String): String { + return getClaimsJws(token) + .body + .subject + } + + fun validateToken(token: String): Unit { + try { + getClaimsJws(token) + } catch (e: SignatureException) { + throw TokenException(INVALID_TOKEN) + } catch (e: MalformedJwtException) { + throw TokenException(WRONG_TOKEN_TYPE) + } catch (e: ExpiredJwtException) { + throw TokenException(EXPIRED_TOKEN) + } catch (e: UnsupportedJwtException) { + throw TokenException(UNSUPPORTED_TOKEN) + } catch (e: IllegalArgumentException) { + throw TokenException(NOT_FOUND_TOKEN) + } + } + + private fun getClaimsJws(token: String) = Jwts.parserBuilder() + .setSigningKey(signingKey.encoded) + .build() + .parseClaimsJws(token) +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/security/LoginUser.kt b/backend/src/main/kotlin/com/dclass/backend/security/LoginUser.kt new file mode 100644 index 0000000000..73b8a4f6d5 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/security/LoginUser.kt @@ -0,0 +1,8 @@ +package com.dclass.backend.security + +import io.swagger.v3.oas.annotations.Hidden + +@Hidden +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +annotation class LoginUser diff --git a/backend/src/main/kotlin/com/dclass/backend/security/LoginUserResolver.kt b/backend/src/main/kotlin/com/dclass/backend/security/LoginUserResolver.kt new file mode 100644 index 0000000000..58242f19c0 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/security/LoginUserResolver.kt @@ -0,0 +1,58 @@ +package com.dclass.backend.security + +import com.dclass.backend.domain.user.User +import com.dclass.backend.domain.user.UserRepository +import com.dclass.backend.domain.user.getByEmailOrThrow +import com.dclass.backend.exception.token.TokenException +import com.dclass.backend.exception.token.TokenExceptionType.* +import org.springframework.core.MethodParameter +import org.springframework.http.HttpHeaders +import org.springframework.stereotype.Component +import org.springframework.web.bind.support.WebDataBinderFactory +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer + +private const val BEARER = "Bearer" + +@Component +class LoginUserResolver( + private val jwtTokenProvider: JwtTokenProvider, + private val userRepository: UserRepository +) : HandlerMethodArgumentResolver { + + override fun supportsParameter(parameter: MethodParameter): Boolean { + return parameter.hasParameterAnnotation(LoginUser::class.java) + } + + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory? + ): User { + val token = extractBearerToken(webRequest) + jwtTokenProvider.validateToken(token) + val userEmail = jwtTokenProvider.getSubject(token) + return userRepository.getByEmailOrThrow(userEmail) + } + + private fun extractBearerToken(request: NativeWebRequest): String { + val authorization = + request.getHeader(HttpHeaders.AUTHORIZATION) ?: throw TokenException(NOT_FOUND_TOKEN) + val (tokenType, token) = splitToTokenFormat(authorization) + if (tokenType != BEARER) { + throw TokenException(WRONG_TOKEN_TYPE) + } + return token + } + + private fun splitToTokenFormat(authorization: String): Pair { + return try { + val tokenFormat = authorization.split(" ") + tokenFormat[0] to tokenFormat[1] + } catch (e: IndexOutOfBoundsException) { + throw TokenException(UNSUPPORTED_TOKEN) + } + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/ui/api/BelongController.kt b/backend/src/main/kotlin/com/dclass/backend/ui/api/BelongController.kt new file mode 100644 index 0000000000..5993b354de --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/ui/api/BelongController.kt @@ -0,0 +1,49 @@ +package com.dclass.backend.ui.api + +import com.dclass.backend.application.BelongService +import com.dclass.backend.application.dto.RemainDurationResponse +import com.dclass.backend.application.dto.SwitchDepartmentResponse +import com.dclass.backend.application.dto.UpdateDepartmentRequest +import com.dclass.backend.domain.user.User +import com.dclass.backend.security.LoginUser +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import jakarta.validation.Valid +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@RequestMapping("/api/belongs") +@RestController +class BelongController( + private val belongService: BelongService, +) { + + @Operation(summary = "활성화된 학과 변경 API", description = "활성화된 학과를 변경합니다") + @ApiResponse(responseCode = "200", description = "학과 변경 성공") + @PutMapping("/switch-departments") + fun switchDepartments( + @LoginUser user: User + ): ResponseEntity { + return ResponseEntity.ok(belongService.switchDepartment(user.id)) + } + + @Operation(summary = "학과 변경 API", description = "학과를 변경합니다") + @ApiResponse(responseCode = "204", description = "학과 변경 성공") + @PutMapping("/change-departments") + fun changeDepartments( + @RequestBody @Valid request: UpdateDepartmentRequest, + @LoginUser user: User + ): ResponseEntity { + belongService.editDepartments(user.id, request) + return ResponseEntity.noContent().build() + } + + @Operation(summary = "남은 학과 변경 일수 조회 API", description = "남은 학과 변경 일수를 조회합니다") + @ApiResponse(responseCode = "200", description = "남은 학과 변경 일수 조회 성공") + @GetMapping("/remain") + fun remain( + @LoginUser user: User + ): ResponseEntity { + return ResponseEntity.ok(belongService.remain(user.id)) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/ui/api/BlocklistController.kt b/backend/src/main/kotlin/com/dclass/backend/ui/api/BlocklistController.kt new file mode 100644 index 0000000000..37524bf10b --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/ui/api/BlocklistController.kt @@ -0,0 +1,26 @@ +package com.dclass.backend.ui.api + +import com.dclass.backend.application.BlocklistService +import com.dclass.backend.application.dto.RemainDurationResponse +import com.dclass.backend.domain.user.User +import com.dclass.backend.security.LoginUser +import io.swagger.v3.oas.annotations.Operation +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RequestMapping("/api/blocklists") +@RestController +class BlocklistController( + private val blocklistService: BlocklistService +) { + + @Operation(summary = "남은 정지 일수 조회 API", description = "남은 정지 일수를 조회합니다") + @GetMapping("/remain") + fun remain( + @LoginUser user: User + ): ResponseEntity { + return ResponseEntity.ok(blocklistService.remain(user.id)) + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/ui/api/CommentController.kt b/backend/src/main/kotlin/com/dclass/backend/ui/api/CommentController.kt new file mode 100644 index 0000000000..4960126437 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/ui/api/CommentController.kt @@ -0,0 +1,76 @@ +package com.dclass.backend.ui.api + +import com.dclass.backend.application.CommentService +import com.dclass.backend.application.dto.* +import com.dclass.backend.domain.user.User +import com.dclass.backend.security.LoginUser +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@Tag(name = "Comment", description = "댓글 관련 API 명세") +@RequestMapping("/api/comments") +@RestController +class CommentController( + private val commentService: CommentService +) { + + @Operation(summary = "댓글 생성 API", description = "게시글에 댓글을 생성합니다.") + @ApiResponse(responseCode = "200", description = "댓글 생성 성공") + @PostMapping + fun createComment( + @LoginUser user: User, + @RequestBody @Valid request: CreateCommentRequest + ): ResponseEntity { + val comment = commentService.create(user.id, request) + return ResponseEntity.ok(comment) + } + + @Operation(summary = "댓글 수정 API", description = "댓글을 수정합니다.") + @ApiResponse(responseCode = "204", description = "댓글 수정 성공") + @PutMapping("/{commentId}") + fun updateComment( + @LoginUser user: User, + @PathVariable commentId: Long, + @RequestBody request: CommentRequest + ): ResponseEntity { + commentService.update(user.id, UpdateCommentRequest(commentId, request.content)) + return ResponseEntity.noContent().build() + } + + @Operation(summary = "댓글 삭제 API", description = "댓글을 삭제합니다.") + @ApiResponse(responseCode = "204", description = "댓글 삭제 성공") + @DeleteMapping("/{commentId}") + fun deleteComment( + @LoginUser user: User, + @PathVariable commentId: Long, + ): ResponseEntity { + commentService.delete(user.id, DeleteCommentRequest(commentId)) + return ResponseEntity.noContent().build() + } + + @Operation(summary = "댓글 조회 API", description = "게시글에 달린 댓글을 조회합니다.") + @ApiResponse(responseCode = "200", description = "댓글 조회 성공") + @GetMapping + fun getComments( + @LoginUser user: User, + request : CommentScrollPageRequest + ): ResponseEntity { + val comments = commentService.findAllByPostId(user.id,request) + return ResponseEntity.ok(comments) + } + + @Operation(summary = "댓글 좋아요 API", description = "댓글에 좋아요를 누릅니다.") + @ApiResponse(responseCode = "204", description = "댓글 좋아요 성공") + @PostMapping("/likes") + fun likeComment( + @LoginUser user: User, + @RequestBody request: LikeCommentRequest + ): ResponseEntity { + commentService.like(user.id, request) + return ResponseEntity.noContent().build() + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/ui/api/HealthCheckController.kt b/backend/src/main/kotlin/com/dclass/backend/ui/api/HealthCheckController.kt new file mode 100644 index 0000000000..7334c5def8 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/ui/api/HealthCheckController.kt @@ -0,0 +1,14 @@ +package com.dclass.backend.ui.api + +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RequestMapping("/api/health-check") +@RestController +class HealthCheckController { + @RequestMapping + fun healthCheck(): ResponseEntity { + return ResponseEntity.ok("Service is up and running!") + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/ui/api/NotificationController.kt b/backend/src/main/kotlin/com/dclass/backend/ui/api/NotificationController.kt new file mode 100644 index 0000000000..0120a87edb --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/ui/api/NotificationController.kt @@ -0,0 +1,40 @@ +package com.dclass.backend.ui.api + +import com.dclass.backend.application.NotificationService +import com.dclass.backend.domain.user.User +import com.dclass.backend.security.LoginUser +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter + +@RestController +@RequestMapping("/api/notifications") +class NotificationController( + private val notificationService: NotificationService +) { + + @Operation(summary = "알림 구독 API", description = "알림을 구독합니다") + @ApiResponse(responseCode = "200", description = "알림 구독 성공") + @GetMapping("/subscribe", produces = [MediaType.TEXT_EVENT_STREAM_VALUE]) + fun subscribe( + @LoginUser user: User, + @RequestHeader(value = "Last-Event_ID", required = false, defaultValue = "") lastEventId: String + ): ResponseEntity { + val emitter = notificationService.subscribe(user.id, lastEventId) + return ResponseEntity.ok(emitter) + } + + @Operation(summary = "알림 조회 API", description = "알림을 조회합니다") + @ApiResponse(responseCode = "200", description = "알림 조회 성공") + @GetMapping("/{notificationId}") + fun readNotification( + @LoginUser user: User, + @PathVariable notificationId: Long + ): ResponseEntity { + notificationService.readNotification(notificationId) + return ResponseEntity.ok().build() + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/ui/api/PostController.kt b/backend/src/main/kotlin/com/dclass/backend/ui/api/PostController.kt new file mode 100644 index 0000000000..3dca0041ba --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/ui/api/PostController.kt @@ -0,0 +1,112 @@ +package com.dclass.backend.ui.api + +import com.dclass.backend.application.PostService +import com.dclass.backend.application.dto.* +import com.dclass.backend.domain.user.User +import com.dclass.backend.security.LoginUser +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@Tag(name = "Post", description = "게시글 관련 API 명세") +@RequestMapping("/api/post") +@RestController +class PostController( + private val postService: PostService +) { + + @Operation(summary = "게시글 조회 API", description = "게시글을 조회합니다.") + @ApiResponse(responseCode = "200", description = "게시글 조회 성공") + @GetMapping("/{postId}") + fun getPost( + @LoginUser user: User, + @PathVariable postId: Long + ): ResponseEntity { + val postResponse = postService.getById(user.id, postId) + return ResponseEntity.ok(postResponse) + } + + @Operation(summary = "게시글 목록 조회 API", description = "게시글 목록을 조회합니다.") + @ApiResponse(responseCode = "200", description = "게시글 목록 조회 성공") + @GetMapping + fun getPosts( + @LoginUser user: User, + request: PostScrollPageRequest + ): ResponseEntity { + return ResponseEntity.ok(postService.getAll(user.id, request)) + } + + @Operation(summary = "내 게시글 목록 조회 API", description = "내 게시글 목록을 조회합니다.") + @ApiResponse(responseCode = "200", description = "내 게시글 목록 조회 성공") + @GetMapping("/mine") + fun getMyPosts( + @LoginUser user: User, + request: PostScrollPageRequest + ): ResponseEntity { + return ResponseEntity.ok(postService.getByUserId(user.id, request)) + } + + @Operation(summary = "스크랩한 게시글 목록 조회 API", description = "스크랩한 게시글 목록을 조회합니다.") + @ApiResponse(responseCode = "200", description = "스크랩한 게시글 목록 조회 성공") + @GetMapping("/scrapped") + fun getScrappedPosts( + @LoginUser user: User, + request: PostScrollPageRequest + ): ResponseEntity { + return ResponseEntity.ok(postService.getScrapped(user.id, request)) + } + + @Operation(summary = "댓글 단 게시글 목록 조회 API", description = "댓글 단 게시글 목록을 조회합니다.") + @ApiResponse(responseCode = "200", description = "댓글 단 게시글 목록 조회 성공") + @GetMapping("/commented") + fun getCommentedPosts( + @LoginUser user: User, + request: PostScrollPageRequest + ): ResponseEntity { + return ResponseEntity.ok(postService.getCommentedAndReplied(user.id, request)) + } + + @Operation(summary = "게시글 생성 API", description = "게시글을 생성합니다.") + @ApiResponse(responseCode = "200", description = "게시글 생성 성공") + @PostMapping + fun createPost( + @LoginUser user: User, + @RequestBody request: CreatePostRequest + ): ResponseEntity { + return ResponseEntity.ok(postService.create(user.id, request)) + } + + @Operation(summary = "게시글 좋아요 API", description = "게시글에 좋아요를 누릅니다.") + @ApiResponse(responseCode = "200", description = "게시글 좋아요 성공") + @PutMapping("/{postId}") + fun updateLikes( + @LoginUser user: User, + @PathVariable postId: Long + ): ResponseEntity { + return ResponseEntity.ok(postService.likes(user.id, postId)) + } + + @Operation(summary = "게시글 수정 API", description = "게시글을 수정합니다.") + @ApiResponse(responseCode = "204", description = "게시글 수정 성공") + @PutMapping + fun updatePost( + @LoginUser user: User, + @RequestBody request: UpdatePostRequest + ): ResponseEntity { + return ResponseEntity.ok(postService.update(user.id, request)) + } + + @Operation(summary = "게시글 삭제 API", description = "게시글을 삭제합니다.") + @ApiResponse(responseCode = "204", description = "게시글 삭제 성공") + @DeleteMapping("/{postId}") + fun deletePost( + @LoginUser user: User, + @PathVariable postId: Long + ): ResponseEntity { + postService.delete(user.id, DeletePostRequest(postId)) + return ResponseEntity.noContent().build() + } +} + diff --git a/backend/src/main/kotlin/com/dclass/backend/ui/api/ReplyController.kt b/backend/src/main/kotlin/com/dclass/backend/ui/api/ReplyController.kt new file mode 100644 index 0000000000..f992160d6b --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/ui/api/ReplyController.kt @@ -0,0 +1,64 @@ +package com.dclass.backend.ui.api + +import com.dclass.backend.application.ReplyService +import com.dclass.backend.application.dto.* +import com.dclass.backend.domain.user.User +import com.dclass.backend.security.LoginUser +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@Tag(name = "Reply", description = "대댓글 관련 API 명세") +@RequestMapping("/api/replies") +@RestController +class ReplyController( + private val replyService: ReplyService +) { + + @Operation(summary = "대댓글 생성 API", description = "댓글에 대댓글을 생성합니다.") + @ApiResponse(responseCode = "200", description = "대댓글 생성 성공") + @PostMapping + fun createReply( + @LoginUser user: User, + @RequestBody request: CreateReplyRequest + ): ResponseEntity { + val reply = replyService.create(user.id, request) + return ResponseEntity.ok(reply) + } + + @Operation(summary = "대댓글 수정 API", description = "대댓글을 수정합니다.") + @ApiResponse(responseCode = "204", description = "대댓글 수정 성공") + @PutMapping("/{replyId}") + fun updateReply( + @LoginUser user: User, + @PathVariable replyId: Long, + @RequestBody request: ReplyRequest + ): ResponseEntity { + replyService.update(user.id, UpdateReplyRequest(replyId, request.content)) + return ResponseEntity.noContent().build() + } + + @Operation(summary = "대댓글 삭제 API", description = "대댓글을 삭제합니다.") + @ApiResponse(responseCode = "204", description = "대댓글 삭제 성공") + @DeleteMapping("/{replyId}") + fun deleteReply( + @LoginUser user: User, + @PathVariable replyId: Long + ): ResponseEntity { + replyService.delete(user.id, DeleteReplyRequest(replyId)) + return ResponseEntity.noContent().build() + } + + @Operation(summary = "대댓글 좋아요 API", description = "대댓글에 좋아요를 누릅니다.") + @ApiResponse(responseCode = "204", description = "대댓글 좋아요 성공") + @PostMapping("/likes") + fun likeReply( + @LoginUser user: User, + @RequestBody request: LikeReplyRequest + ): ResponseEntity { + replyService.like(user.id, request) + return ResponseEntity.noContent().build() + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/ui/api/ReportController.kt b/backend/src/main/kotlin/com/dclass/backend/ui/api/ReportController.kt new file mode 100644 index 0000000000..4fafb54444 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/ui/api/ReportController.kt @@ -0,0 +1,26 @@ +package com.dclass.backend.ui.api + +import com.dclass.backend.application.ReportService +import com.dclass.backend.application.dto.UpdateReportRequest +import com.dclass.backend.domain.user.User +import com.dclass.backend.security.LoginUser +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RequestMapping("/api/report") +@RestController +class ReportController( + private val reportService: ReportService +) { + @PostMapping + fun update( + @LoginUser user: User, + @RequestBody request: UpdateReportRequest + ): ResponseEntity { + reportService.report(user.id, request) + return ResponseEntity.noContent().build() + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/ui/api/ScrapController.kt b/backend/src/main/kotlin/com/dclass/backend/ui/api/ScrapController.kt new file mode 100644 index 0000000000..ed60c2de16 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/ui/api/ScrapController.kt @@ -0,0 +1,43 @@ +package com.dclass.backend.ui.api + +import com.dclass.backend.application.ScrapService +import com.dclass.backend.application.dto.PostResponse +import com.dclass.backend.domain.user.User +import com.dclass.backend.security.LoginUser +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@Tag(name = "Scrap", description = "스크랩 관련 API 명세") +@RequestMapping("/api/scrap") +@RestController +class ScrapController( + private val scrapService: ScrapService +) { + + @Operation(summary = "스크랩 조회 API", description = "스크랩한 게시물을 조회합니다.") + @ApiResponse(responseCode = "200", description = "스크랩 조회 성공") + @GetMapping + fun readAll(@LoginUser user: User): ResponseEntity> { + val scrap = scrapService.getAll(user.id) + return ResponseEntity.ok(scrap) + } + + @Operation(summary = "스크랩 생성 API", description = "게시물을 스크랩합니다.") + @ApiResponse(responseCode = "204", description = "스크랩 생성 성공") + @PostMapping + fun create(@LoginUser user: User, postId: Long): ResponseEntity { + scrapService.create(user.id, postId) + return ResponseEntity.noContent().build() + } + + @Operation(summary = "스크랩 삭제 API", description = "스크랩한 게시물을 삭제합니다.") + @ApiResponse(responseCode = "204", description = "스크랩 삭제 성공") + @DeleteMapping("/{postId}") + fun delete(@LoginUser user: User, @PathVariable postId: Long): ResponseEntity { + scrapService.delete(user.id, postId) + return ResponseEntity.noContent().build() + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/ui/api/UserBlockController.kt b/backend/src/main/kotlin/com/dclass/backend/ui/api/UserBlockController.kt new file mode 100644 index 0000000000..46b78e27ab --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/ui/api/UserBlockController.kt @@ -0,0 +1,27 @@ +package com.dclass.backend.ui.api + +import com.dclass.backend.application.UserBlockService +import com.dclass.backend.domain.user.User +import com.dclass.backend.security.LoginUser +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@Tag(name = "UserBlock", description = "유저 차단 관련 API 명세") +@RequestMapping("/api/block") +@RestController +class UserBlockController( + private val userBlockService: UserBlockService +) { + @Operation(summary = "유저 차단 API", description = "사용자를 차단합니다.") + @ApiResponse(responseCode = "200", description = "사용자 차단 성공") + @PostMapping + fun block(@LoginUser user: User, blockedUserId: Long): ResponseEntity { + userBlockService.block(user.id, blockedUserId) + return ResponseEntity.noContent().build() + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/backend/ui/api/UserRestController.kt b/backend/src/main/kotlin/com/dclass/backend/ui/api/UserRestController.kt new file mode 100644 index 0000000000..4862278a02 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/backend/ui/api/UserRestController.kt @@ -0,0 +1,129 @@ +package com.dclass.backend.ui.api + +import com.dclass.backend.application.BlacklistService +import com.dclass.backend.application.UserAuthenticationService +import com.dclass.backend.application.UserService +import com.dclass.backend.application.dto.* +import com.dclass.backend.application.mail.MailService +import com.dclass.backend.config.ACCESS_TOKEN_SECURITY_SCHEME_KEY +import com.dclass.backend.domain.user.User +import com.dclass.backend.security.AuthToken +import com.dclass.backend.security.LoginUser +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@Tag(name = "User", description = "유저 관련 API 명세") +@RequestMapping("/api/users") +@RestController +class UserRestController( + private val userService: UserService, + private val userAuthenticationService: UserAuthenticationService, + private val blacklistService: BlacklistService, + private val mailService: MailService +) { + + @Operation(summary = "회원가입 API", description = "이메일 인증 후 회원가입을 진행합니다") + @ApiResponse(responseCode = "200", description = "회원가입 성공") + @PostMapping("/register") + fun generateToken(@RequestBody @Valid request: RegisterUserRequest): ResponseEntity { + val token = userAuthenticationService.generateTokenByRegister(request) + return ResponseEntity.ok(token) + } + + @Operation(summary = "로그인 API", description = "로그인을 진행합니다") + @ApiResponse(responseCode = "200", description = "로그인 성공") + @PostMapping("/login") + fun generateToken(@RequestBody @Valid request: AuthenticateUserRequest): ResponseEntity { + val token = userAuthenticationService.generateTokenByLogin(request) + return ResponseEntity.ok(token) + } + + @Operation(summary = "토큰 재발급 API", description = "리프레시 토큰을 이용하여 토큰을 재발급합니다") + @ApiResponse(responseCode = "200", description = "토큰 재발급 성공") + @PostMapping("/reissue-token") + fun generateToken(@AuthToken refreshToken: String): ResponseEntity { + val token = blacklistService.reissueToken(refreshToken) + return ResponseEntity.ok(token) + } + + @Operation(summary = "비밀번호 재설정 API", description = "비밀번호를 재설정한 후 변경된 비밀번호를 이메일로 전송합니다") + @ApiResponse(responseCode = "204", description = "비밀번호 재설정 성공") + @PostMapping("/reset-password") + fun resetPassword(@RequestBody @Valid request: ResetPasswordRequest): ResponseEntity { + userService.resetPassword(request) + return ResponseEntity.noContent().build() + } + + @Operation(summary = "비밀번호 변경 API", description = "비밀번호를 변경합니다") + @ApiResponse(responseCode = "204", description = "비밀번호 변경 성공") + @SecurityRequirement(name = ACCESS_TOKEN_SECURITY_SCHEME_KEY) + @PostMapping("/edit-password") + fun editPassword( + @RequestBody @Valid request: EditPasswordRequest, + @LoginUser user: User + ): ResponseEntity { + userService.editPassword(user.id, request) + return ResponseEntity.noContent().build() + } + + @Operation(summary = "이메일 인증코드 발급 API", description = "이메일로 인증코드를 발급합니다") + @ApiResponse(responseCode = "204", description = "이메일 인증코드 발급 성공") + @PostMapping("/authentication-code") + fun generateAuthenticationCode( + @RequestParam email: String + ): ResponseEntity { + val authenticationCode = userAuthenticationService + .generateAuthenticationCode(email) + mailService.sendAuthenticationCodeMail(email, authenticationCode) + return ResponseEntity.noContent().build() + } + + @Operation(summary = "이메일 인증 API", description = "이메일을 인증합니다") + @ApiResponse(responseCode = "204", description = "이메일 인증 성공") + @PostMapping("/authenticate-email") + fun authenticateEmail( + @RequestParam email: String, + @RequestParam authenticationCode: String + ): ResponseEntity { + userAuthenticationService.authenticateEmail(email, authenticationCode) + return ResponseEntity.noContent().build() + } + + @Operation(summary = "내 정보 조회 API", description = "내 정보를 조회합니다") + @ApiResponse(responseCode = "200", description = "내 정보 조회 성공") + @SecurityRequirement(name = ACCESS_TOKEN_SECURITY_SCHEME_KEY) + @GetMapping("/me") + fun getMyInformation( + @LoginUser user: User + ): ResponseEntity { + val response = userService.getInformation(user.id) + return ResponseEntity.ok(response) + } + + + @Operation(summary = "닉네임 변경 API", description = "닉네임을 변경합니다") + @ApiResponse(responseCode = "204", description = "닉네임 변경 성공") + @PutMapping("/change-nickname") + fun changeNickname( + @RequestBody @Valid request: UpdateNicknameRequest, + @LoginUser user: User + ): ResponseEntity { + userService.editNickname(user.id, request) + return ResponseEntity.noContent().build() + } + + @Operation(summary = "회원 탈퇴 API", description = "회원 탈퇴를 진행합니다") + @ApiResponse(responseCode = "204", description = "회원 탈퇴 성공") + @PostMapping("/resign") + fun resign( + @LoginUser user: User + ): ResponseEntity { + userService.resign(user.id) + return ResponseEntity.noContent().build() + } +} \ No newline at end of file diff --git a/backend/src/main/kotlin/com/dclass/support/domain/BaseEntities.kt b/backend/src/main/kotlin/com/dclass/support/domain/BaseEntities.kt new file mode 100644 index 0000000000..d8fecaf9ab --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/support/domain/BaseEntities.kt @@ -0,0 +1,34 @@ +package com.dclass.support.domain + +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.MappedSuperclass +import org.springframework.data.domain.AbstractAggregateRoot +import java.util.* + +@MappedSuperclass +abstract class BaseEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L +) { + override fun equals(other: Any?): Boolean = + Objects.equals(id, (other as? BaseEntity)?.id) + + override fun hashCode(): Int = + Objects.hashCode(id) +} + +@MappedSuperclass +abstract class BaseRootEntity>( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L +) : AbstractAggregateRoot() { + override fun equals(other: Any?): Boolean = + Objects.equals(id, (other as? BaseRootEntity)?.id) + + override fun hashCode(): Int = + Objects.hashCode(id) +} diff --git a/backend/src/main/kotlin/com/dclass/support/domain/Image.kt b/backend/src/main/kotlin/com/dclass/support/domain/Image.kt new file mode 100644 index 0000000000..c7a65b7ba4 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/support/domain/Image.kt @@ -0,0 +1,10 @@ +package com.dclass.support.domain + +import jakarta.persistence.Column +import jakarta.persistence.Embeddable + +@Embeddable +class Image( + @Column(nullable = false) + val imageKey: String +) diff --git a/backend/src/main/kotlin/com/dclass/support/security/Encrypts.kt b/backend/src/main/kotlin/com/dclass/support/security/Encrypts.kt new file mode 100644 index 0000000000..d433cd3b95 --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/support/security/Encrypts.kt @@ -0,0 +1,10 @@ +package com.dclass.support.security + +import java.security.MessageDigest + +private val SHA256: MessageDigest = MessageDigest.getInstance("SHA-256") + +fun sha256Encrypt(plainText: String): String = bytesToHex(SHA256.digest(plainText.toByteArray())) + +private fun bytesToHex(bytes: ByteArray): String = + bytes.fold("") { previous, current -> previous + "%02x".format(current) } diff --git a/backend/src/main/kotlin/com/dclass/support/util/logger.kt b/backend/src/main/kotlin/com/dclass/support/util/logger.kt new file mode 100644 index 0000000000..d85f594edf --- /dev/null +++ b/backend/src/main/kotlin/com/dclass/support/util/logger.kt @@ -0,0 +1,8 @@ +package com.dclass.support.util + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +inline fun T.logger(): Logger { + return LoggerFactory.getLogger(T::class.java) +} \ No newline at end of file diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml new file mode 100644 index 0000000000..2877b4133f --- /dev/null +++ b/backend/src/main/resources/application-dev.yml @@ -0,0 +1,42 @@ +spring: + config: + activate: + on-profile: "dev" + + flyway: + enabled: false + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/dclass?serverTimezone=UTC + username: dclass-user + password: password + + + jpa: + hibernate: + ddl-auto: create-drop + + properties: + hibernate: + show_sql: true + format_sql: true + default_batch_fetch_size: 10 + +logging: + level: + org.hibernate.orm.jdbc.bind: trace + +jwt: + secret: "1cf848f58e0e538db479ee4c0ea6df2e152da2688a0f8ed63aeb3e17359c521122ad20efc5f28cd6943ef86363c17f72eb4c7482bdae6b749f80cc9e23a07fe4" + +springdoc: + packages-to-scan: com.dclass.backend.ui + default-consumes-media-type: application/json;charset=UTF-8 + default-produces-media-type: application/json;charset=UTF-8 + swagger-ui: + path: / + disable-swagger-default-url: true + display-request-duration: true + operations-sorter: alpha +--- \ No newline at end of file diff --git a/backend/src/main/resources/application-test.yml b/backend/src/main/resources/application-test.yml new file mode 100644 index 0000000000..447357c435 --- /dev/null +++ b/backend/src/main/resources/application-test.yml @@ -0,0 +1,24 @@ +spring: + config: + activate: + on-profile: "test" + + flyway: + enabled: false + + datasource: + url: jdbc:h2:mem:testdb;Mode=MySQL + + jpa: + hibernate: + ddl-auto: create-drop + + properties: + hibernate: + show_sql: true + format_sql: true + default_batch_fetch_size: 10 + +jwt: + secret: "1cf848f58e0e538db479ee4c0ea6df2e152da2688a0f8ed63aeb3e17359c521122ad20efc5f28cd6943ef86363c17f72eb4c7482bdae6b749f80cc9e23a07fe4" +--- \ No newline at end of file diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 0000000000..761fe59b38 --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,13 @@ +spring: + profiles: + group: + "local": "dev" + + config: + import: + - classpath:application-security.yml + - classpath:application-dev.yml + - classpath:application-test.yml + - classpath:application-prod.yml +--- + diff --git a/backend/src/main/resources/db/migration/V1__init.sql b/backend/src/main/resources/db/migration/V1__init.sql new file mode 100644 index 0000000000..c041f1a92e --- /dev/null +++ b/backend/src/main/resources/db/migration/V1__init.sql @@ -0,0 +1,792 @@ +create table authentication_code +( + authenticated bit not null, + deleted bit not null, + created_date_time datetime(6) not null, + id bigint not null auto_increment, + code char(6) not null, + email varchar(255) not null, + primary key (id) +) engine=InnoDB; + +create table belong +( + deleted bit not null, + major_index integer not null, + id bigint not null auto_increment, + modified_date_time datetime(6) not null, + user_id bigint not null, + primary key (id), + constraint UK_belong_user_id unique (user_id) +) engine=InnoDB; + +create table blacklist +( + deleted bit not null, + id bigint not null auto_increment, + invalid_refresh_token varchar(255) not null, + primary key (id) +) engine=InnoDB; + +create table comment +( + deleted bit not null, + created_date_time datetime(6) not null, + id bigint not null auto_increment, + modified_date_time datetime(6) not null, + post_id bigint not null, + user_id bigint not null, + content varchar(255) not null, + reply_count integer not null, + primary key (id) +) engine=InnoDB; + +create table comment_likes +( + comment_id bigint not null, + users_id bigint not null, + constraint FK_comment_likes_comment_id foreign key (comment_id) references comment (id) +) engine=InnoDB; + +create table community +( + deleted bit not null, + department_id bigint not null, + id bigint not null auto_increment, + title varchar(100) not null, + description varchar(255) not null, + primary key (id) +) engine=InnoDB; + +create table department +( + deleted bit not null, + id bigint not null auto_increment, + title varchar(255) not null, + primary key (id) +) engine=InnoDB; + +create table join_department +( + _department_ids bigint, + belong_id bigint not null, + constraint FK_join_department_belong_id foreign key (belong_id) references belong (id) +) engine=InnoDB; + +create table post +( + comment_reply_count integer, + deleted bit not null, + is_question bit not null, + like_count integer, + scrap_count integer, + community_id bigint not null, + created_date_time datetime(6) not null, + id bigint not null auto_increment, + modified_date_time datetime(6) not null, + user_id bigint not null, + title varchar(100) not null, + content varchar(255) not null, + primary key (id) +) engine=InnoDB; + +create table post_images +( + post_id bigint not null, + image_key varchar(255) not null, + constraint FK_post_images_post_id foreign key (post_id) references post (id) +) engine=InnoDB; + +create table post_likes +( + post_id bigint not null, + users_id bigint not null, + constraint FK_post_likes_post_id foreign key (post_id) references post (id) +) engine=InnoDB; + +create table reply +( + deleted bit not null, + comment_id bigint not null, + created_date_time datetime(6) not null, + id bigint not null auto_increment, + modified_date_time datetime(6) not null, + user_id bigint not null, + content varchar(255) not null, + primary key (id) +) engine=InnoDB; + +create table reply_likes +( + reply_id bigint not null, + users_id bigint not null, + constraint FK_reply_likes_reply_id foreign key (reply_id) references reply (id) +) engine=InnoDB; + +create table scrap +( + deleted bit not null, + id bigint not null auto_increment, + post_id bigint not null, + user_id bigint not null, + primary key (id) +) engine=InnoDB; + +create table university +( + id bigint not null auto_increment, + email_suffix varchar(255) not null, + logo varchar(255) not null, + name varchar(255) not null, + primary key (id) +) engine=InnoDB; + +create table users +( + deleted bit not null, + id bigint not null auto_increment, + university_id bigint not null, + nickname varchar(13) not null, + name varchar(30) not null, + email varchar(255) not null, + password varchar(255) not null, + primary key (id), + constraint UK_users_email unique (email), + constraint FK_users_university_id foreign key (university_id) references university (id) +) engine=InnoDB; + +create table notifications +( + is_read bit not null, + created_at datetime(6) not null, + id bigint not null auto_increment, + post_id bigint not null, + user_id bigint not null, + content varchar(255) not null, + type enum ('COMMENT','REPLY') not null, + primary key (id) +) engine=InnoDB; + +INSERT INTO department (deleted, title) VALUES (0, '언어정보학과'); +INSERT INTO department (deleted, title) VALUES (0, '국어국문학과'); +INSERT INTO department (deleted, title) VALUES (0, '독어독문학과'); +INSERT INTO department (deleted, title) VALUES (0, '노어노문학과'); +INSERT INTO department (deleted, title) VALUES (0, '영어영문학과'); +INSERT INTO department (deleted, title) VALUES (0, '일어일문학과'); +INSERT INTO department (deleted, title) VALUES (0, '중어중문학과'); +INSERT INTO department (deleted, title) VALUES (0, '불어불문학과'); +INSERT INTO department (deleted, title) VALUES (0, '서어서문학과'); +INSERT INTO department (deleted, title) VALUES (0, '북한학과'); +INSERT INTO department (deleted, title) VALUES (0, '철학과'); +INSERT INTO department (deleted, title) VALUES (0, '사학과'); +INSERT INTO department (deleted, title) VALUES (0, '문화인류학과'); +INSERT INTO department (deleted, title) VALUES (0, '문예창작학과'); +INSERT INTO department (deleted, title) VALUES (0, '문헌정보학과'); +INSERT INTO department (deleted, title) VALUES (0, '관광학과'); +INSERT INTO department (deleted, title) VALUES (0, '한문학과'); +INSERT INTO department (deleted, title) VALUES (0, '신학과'); +INSERT INTO department (deleted, title) VALUES (0, '불교학과'); +INSERT INTO department (deleted, title) VALUES (0, '자율전공학과'); +INSERT INTO department (deleted, title) VALUES (0, '경영학과'); +INSERT INTO department (deleted, title) VALUES (0, '경제학과'); +INSERT INTO department (deleted, title) VALUES (0, '경영정보학과'); +INSERT INTO department (deleted, title) VALUES (0, '국제통상학과'); +INSERT INTO department (deleted, title) VALUES (0, '광고홍보학과'); +INSERT INTO department (deleted, title) VALUES (0, '금융학과'); +INSERT INTO department (deleted, title) VALUES (0, '회계학과'); +INSERT INTO department (deleted, title) VALUES (0, '세무학과'); +INSERT INTO department (deleted, title) VALUES (0, '심리학과'); +INSERT INTO department (deleted, title) VALUES (0, '법학과'); +INSERT INTO department (deleted, title) VALUES (0, '사회학과'); +INSERT INTO department (deleted, title) VALUES (0, '도시학과'); +INSERT INTO department (deleted, title) VALUES (0, '정치외교학과'); +INSERT INTO department (deleted, title) VALUES (0, '국제학과'); +INSERT INTO department (deleted, title) VALUES (0, '사회복지학과'); +INSERT INTO department (deleted, title) VALUES (0, '미디어커뮤니케이션학과'); +INSERT INTO department (deleted, title) VALUES (0, '지리학과'); +INSERT INTO department (deleted, title) VALUES (0, '행정학과'); +INSERT INTO department (deleted, title) VALUES (0, '군사학과'); +INSERT INTO department (deleted, title) VALUES (0, '경찰행정학과'); +INSERT INTO department (deleted, title) VALUES (0, '아동가족학과'); +INSERT INTO department (deleted, title) VALUES (0, '소비자학과'); +INSERT INTO department (deleted, title) VALUES (0, '물류학과'); +INSERT INTO department (deleted, title) VALUES (0, '무역학과'); +INSERT INTO department (deleted, title) VALUES (0, '호텔경영학과'); +INSERT INTO department (deleted, title) VALUES (0, '가정교육과'); +INSERT INTO department (deleted, title) VALUES (0, '건설공학교육과'); +INSERT INTO department (deleted, title) VALUES (0, '과학교육과'); +INSERT INTO department (deleted, title) VALUES (0, '전기전자통신공학교육과'); +INSERT INTO department (deleted, title) VALUES (0, '기계재료공학교육과'); +INSERT INTO department (deleted, title) VALUES (0, '기술교육과'); +INSERT INTO department (deleted, title) VALUES (0, '농업교육과'); +INSERT INTO department (deleted, title) VALUES (0, '물리교육과'); +INSERT INTO department (deleted, title) VALUES (0, '미술교육과'); +INSERT INTO department (deleted, title) VALUES (0, '사회교육과'); +INSERT INTO department (deleted, title) VALUES (0, '생물교육과'); +INSERT INTO department (deleted, title) VALUES (0, '수학교육과'); +INSERT INTO department (deleted, title) VALUES (0, '수해양산업교육과'); +INSERT INTO department (deleted, title) VALUES (0, '아동교육과'); +INSERT INTO department (deleted, title) VALUES (0, '언어치료학과'); +INSERT INTO department (deleted, title) VALUES (0, '언어교육학과'); +INSERT INTO department (deleted, title) VALUES (0, '역사교육과'); +INSERT INTO department (deleted, title) VALUES (0, '음악교육과'); +INSERT INTO department (deleted, title) VALUES (0, '윤리교육과'); +INSERT INTO department (deleted, title) VALUES (0, '종교교육과'); +INSERT INTO department (deleted, title) VALUES (0, '지구과학교육과'); +INSERT INTO department (deleted, title) VALUES (0, '지리교육과'); +INSERT INTO department (deleted, title) VALUES (0, '체육교육과'); +INSERT INTO department (deleted, title) VALUES (0, '초등교육과'); +INSERT INTO department (deleted, title) VALUES (0, '컴퓨터교육과'); +INSERT INTO department (deleted, title) VALUES (0, '특수교육과'); +INSERT INTO department (deleted, title) VALUES (0, '한문교육과'); +INSERT INTO department (deleted, title) VALUES (0, '화학교육과'); +INSERT INTO department (deleted, title) VALUES (0, '환경교육과'); +INSERT INTO department (deleted, title) VALUES (0, '컴퓨터공학과'); +INSERT INTO department (deleted, title) VALUES (0, '조선공학과'); +INSERT INTO department (deleted, title) VALUES (0, '산업공학과'); +INSERT INTO department (deleted, title) VALUES (0, '멀티미디어학과'); +INSERT INTO department (deleted, title) VALUES (0, '게임공학과'); +INSERT INTO department (deleted, title) VALUES (0, '미디어출판과'); +INSERT INTO department (deleted, title) VALUES (0, '재료공학과'); +INSERT INTO department (deleted, title) VALUES (0, '화장품과'); +INSERT INTO department (deleted, title) VALUES (0, '건축학과'); +INSERT INTO department (deleted, title) VALUES (0, '물류시스템공학과'); +INSERT INTO department (deleted, title) VALUES (0, '해양공학과'); +INSERT INTO department (deleted, title) VALUES (0, '고분자공학과'); +INSERT INTO department (deleted, title) VALUES (0, '광학공학과'); +INSERT INTO department (deleted, title) VALUES (0, '교통공학과'); +INSERT INTO department (deleted, title) VALUES (0, '국방기술학과'); +INSERT INTO department (deleted, title) VALUES (0, '금속공학과'); +INSERT INTO department (deleted, title) VALUES (0, '금형설계과'); +INSERT INTO department (deleted, title) VALUES (0, '기계공학과'); +INSERT INTO department (deleted, title) VALUES (0, '나노공학과'); +INSERT INTO department (deleted, title) VALUES (0, '냉동공조공학과'); +INSERT INTO department (deleted, title) VALUES (0, '도시공학과'); +INSERT INTO department (deleted, title) VALUES (0, '로봇공학과'); +INSERT INTO department (deleted, title) VALUES (0, '무인항공학과'); +INSERT INTO department (deleted, title) VALUES (0, '반도체과'); +INSERT INTO department (deleted, title) VALUES (0, '산업설비자동화과'); +INSERT INTO department (deleted, title) VALUES (0, '섬유과'); +INSERT INTO department (deleted, title) VALUES (0, '세라믹공학과'); +INSERT INTO department (deleted, title) VALUES (0, '소방방재학과'); +INSERT INTO department (deleted, title) VALUES (0, '시스템공학과'); +INSERT INTO department (deleted, title) VALUES (0, '신소재공학과'); +INSERT INTO department (deleted, title) VALUES (0, '신재생에너지과'); +INSERT INTO department (deleted, title) VALUES (0, '안전공학과'); +INSERT INTO department (deleted, title) VALUES (0, '에너지자원공학과'); +INSERT INTO department (deleted, title) VALUES (0, '원자력공학과'); +INSERT INTO department (deleted, title) VALUES (0, '자동차과'); +INSERT INTO department (deleted, title) VALUES (0, '전기공학과'); +INSERT INTO department (deleted, title) VALUES (0, '전자공학과'); +INSERT INTO department (deleted, title) VALUES (0, '정보보호학과'); +INSERT INTO department (deleted, title) VALUES (0, '제어계측공학과'); +INSERT INTO department (deleted, title) VALUES (0, '제지공학과'); +INSERT INTO department (deleted, title) VALUES (0, '조경과'); +INSERT INTO department (deleted, title) VALUES (0, '지구해양과학과'); +INSERT INTO department (deleted, title) VALUES (0, '철도교통과'); +INSERT INTO department (deleted, title) VALUES (0, '측지정보과'); +INSERT INTO department (deleted, title) VALUES (0, '토목공학과'); +INSERT INTO department (deleted, title) VALUES (0, '특수장비과'); +INSERT INTO department (deleted, title) VALUES (0, '항공공학과'); +INSERT INTO department (deleted, title) VALUES (0, '화학공학과'); +INSERT INTO department (deleted, title) VALUES (0, '환경화학과'); +INSERT INTO department (deleted, title) VALUES (0, '생명공학과'); +INSERT INTO department (deleted, title) VALUES (0, '조리학과'); +INSERT INTO department (deleted, title) VALUES (0, '원예과'); +INSERT INTO department (deleted, title) VALUES (0, '농수산과'); +INSERT INTO department (deleted, title) VALUES (0, '환경공학과'); +INSERT INTO department (deleted, title) VALUES (0, '동물학과'); +INSERT INTO department (deleted, title) VALUES (0, '제약공학과'); +INSERT INTO department (deleted, title) VALUES (0, '식품학과'); +INSERT INTO department (deleted, title) VALUES (0, '수의학과'); +INSERT INTO department (deleted, title) VALUES (0, '천문학과'); +INSERT INTO department (deleted, title) VALUES (0, '물리학과'); +INSERT INTO department (deleted, title) VALUES (0, '수학과'); +INSERT INTO department (deleted, title) VALUES (0, '화학과'); +INSERT INTO department (deleted, title) VALUES (0, '의류학과'); +INSERT INTO department (deleted, title) VALUES (0, '임산공학'); +INSERT INTO department (deleted, title) VALUES (0, '지질학과'); +INSERT INTO department (deleted, title) VALUES (0, '지리학과'); +INSERT INTO department (deleted, title) VALUES (0, '의예과'); +INSERT INTO department (deleted, title) VALUES (0, '간호학과'); +INSERT INTO department (deleted, title) VALUES (0, '약학과'); +INSERT INTO department (deleted, title) VALUES (0, '치의학과'); +INSERT INTO department (deleted, title) VALUES (0, '물리치료학과'); +INSERT INTO department (deleted, title) VALUES (0, '한의예과'); +INSERT INTO department (deleted, title) VALUES (0, '환경보건학과'); +INSERT INTO department (deleted, title) VALUES (0, '응급구조학과'); +INSERT INTO department (deleted, title) VALUES (0, '의무행정과'); +INSERT INTO department (deleted, title) VALUES (0, '의료장비공학과'); +INSERT INTO department (deleted, title) VALUES (0, '임상병리학과'); +INSERT INTO department (deleted, title) VALUES (0, '방사선과'); +INSERT INTO department (deleted, title) VALUES (0, '소방안전관리과'); +INSERT INTO department (deleted, title) VALUES (0, '예술치료학과'); +INSERT INTO department (deleted, title) VALUES (0, '언어재활과'); +INSERT INTO department (deleted, title) VALUES (0, '순수미술과'); +INSERT INTO department (deleted, title) VALUES (0, '응용미술과'); +INSERT INTO department (deleted, title) VALUES (0, '조형과'); +INSERT INTO department (deleted, title) VALUES (0, '공예과'); +INSERT INTO department (deleted, title) VALUES (0, '공업디자인과'); +INSERT INTO department (deleted, title) VALUES (0, '그래픽디자인과'); +INSERT INTO department (deleted, title) VALUES (0, '미디어영상학과'); +INSERT INTO department (deleted, title) VALUES (0, '애니메이션과'); +INSERT INTO department (deleted, title) VALUES (0, '기악과'); +INSERT INTO department (deleted, title) VALUES (0, '성악과'); +INSERT INTO department (deleted, title) VALUES (0, '음악과'); +INSERT INTO department (deleted, title) VALUES (0, '실용음악과'); +INSERT INTO department (deleted, title) VALUES (0, '연극영화과'); +INSERT INTO department (deleted, title) VALUES (0, '패션디자인과'); +INSERT INTO department (deleted, title) VALUES (0, '실내디자인과'); +INSERT INTO department (deleted, title) VALUES (0, '광고디자인과'); +INSERT INTO department (deleted, title) VALUES (0, '댄스스포츠과'); +INSERT INTO department (deleted, title) VALUES (0, '사회체육과'); +INSERT INTO department (deleted, title) VALUES (0, '경호학과'); +INSERT INTO department (deleted, title) VALUES (0, '건강관리과'); +INSERT INTO department (deleted, title) VALUES (0, '메이크업아티스트과'); +INSERT INTO department (deleted, title) VALUES (0, '모델과'); +INSERT INTO department (deleted, title) VALUES (0, '보석감정과'); +INSERT INTO department (deleted, title) VALUES (0, '산업잠수과'); +INSERT INTO department (deleted, title) VALUES (0, ''); + +INSERT INTO university (email_suffix, logo, name) VALUES ('gachon.ac.kr','','가천길대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('csj.ac.kr','','가톨릭상지대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gangdong.ac.kr','','강동대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gyc.ac.kr','','강릉영동대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kt.ac.kr','','강원관광대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gw.ac.kr','','강원도립대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('koje.ac.kr','','거제대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gtec.ac.kr','','경기과학기술대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gc.ac.kr','','경남도립거창대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('namhae.ac.kr','','경남도립남해대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kit.ac.kr','','경남정보대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kyungmin.ac.kr','','경민대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kbu.ac.kr','','경복대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kbsc.ac.kr','','경북과학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gpc.ac.kr','','경북도립대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kbc.ac.kr','','경북전문대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gs.ac.kr','','경산1대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kwc.ac.kr','','경원전문대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kic.ac.kr','','경인여자대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kmcu.ac.kr','','계명문화대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kaywon.ac.kr','','계원예술대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kgrc.ac.kr','','고구려대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kwangyang.ac.kr','','광양보건대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ghu.ac.kr','','광주보건대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gumi.ac.kr','','구미대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('saotc.ac.kr','','구세군사관학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kookje.ac.kr','','국제대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kcn.ac.kr','','군산간호대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kunjang.ac.kr','','군장대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ccn.ac.kr','','기독간호대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kcs.ac.kr','','김천과학대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gimcheon.ac.kr','','김천대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kimpo.ac.kr','','김포대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gimhae.ac.kr','','김해대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('nonghyup.ac.kr','','농협대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('tk.ac.kr','','대경대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ttc.ac.kr','','대구공업대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('tsu.ac.kr','','대구과학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dfc.ac.kr','','대구미래대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dhc.ac.kr','','대구보건대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ddu.ac.kr','','대덕대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('daedong.ac.kr','','대동대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('daelim.ac.kr','','대림대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('daewon.ac.kr','','대원대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hit.ac.kr','','대전보건대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dkc.ac.kr','','동강대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dongnam.ac.kr','','동남보건대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('tu.ac.kr','','동명대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dpc.ac.kr','','동부산대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dsc.ac.kr','','동서울대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dima.ac.kr','','동아방송예술대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dongac.ac.kr','','동아인재대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dongyang.ac.kr','','동양미래대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('duc.ac.kr','','동우대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dist.ac.kr','','동원과학기술대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('tw.ac.kr','','동원대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dit.ac.kr','','동의과학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dongju.ac.kr','','동주대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('doowon.ac.kr','','두원공과대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('masan.ac.kr','','마산대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('mjc.ac.kr','','명지전문대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('mokpo-c.ac.kr','','목포과학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('mkc.ac.kr','','문경대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('baewha.ac.kr','','배화여자대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('bscu.ac.kr','','백석문화대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('paekche.ac.kr','','백제예술대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('bs.ac.kr','','벽성대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('bsks.ac.kr','','부산경상대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('bist.ac.kr','','부산과학기술대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('bwc.ac.kr','','부산여자대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('busanarts.ac.kr','','부산예술대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('bc.ac.kr','','부천대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('shu.ac.kr','','삼육보건대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('syu.ac.kr','','삼육의명대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sy.ac.kr','','상지영서대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sorabol.ac.kr','','서라벌대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('seoyeong.ac.kr','','서영대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('shjc.ac.kr','','서울보건대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('snjc.ac.kr','','서울여자간호대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('seoularts.ac.kr','','서울예술대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('seoil.ac.kr','','서일대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('seojeong.ac.kr','','서정대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sohae.ac.kr','','서해대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sunlin.ac.kr','','선린대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sdc.ac.kr','','성덕대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sungsim.ac.kr','','성심외국어대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('saekyung.ac.kr','','세경대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('songgok.ac.kr','','송곡대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('songwon.ac.kr','','송원대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('songho.ac.kr','','송호대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sc.ac.kr','','수성대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ssc.ac.kr','','수원과학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('swc.ac.kr','','수원여자대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('suncheon.ac.kr','','순천제일대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sewc.ac.kr','','숭의여자대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('shingu.ac.kr','','신구대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('shinsung.ac.kr','','신성대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sau.ac.kr','','신안산대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('shc.ac.kr','','신흥대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('motor.ac.kr','','아주자동차대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('asc.ac.kr','','안동과학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ansan.ac.kr','','안산대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('yit.ac.kr','','여주대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('yeonsung.ac.kr','','연성대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('yc.ac.kr','','연암공과대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('yflc.ac.kr','','영남외국어대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ync.ac.kr','','영남이공대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ycc.ac.kr','','영진사이버대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('yjc.ac.kr','','영진전문대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('osan.ac.kr','','오산대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ysc.ac.kr','','용인송담대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('wst.ac.kr','','우송공업대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('wsi.ac.kr','','우송정보대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('uc.ac.kr','','울산과학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('wat.ac.kr','','웅지세무대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('wkhc.ac.kr','','원광보건대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('wonju.ac.kr','','원주대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('yuhan.ac.kr','','유한대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('induk.ac.kr','','인덕대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('jeiu.ac.kr','','인천재능대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('icc.ac.kr','','인천전문대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('itc.ac.kr','','인하공업전문대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('jangan.ac.kr','','장안대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('cau.ac.kr','','적십자간호대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('chunnam-c.ac.kr','','전남과학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dorip.ac.kr','','전남도립대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('jbsc.ac.kr','','전북과학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('jk.ac.kr','','전주기전대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('jvision.ac.kr','','전주비전대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ctc.ac.kr','','제주관광대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('jeju.ac.kr','','제주산업정보대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('chu.ac.kr','','제주한라대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('cnc.ac.kr','','조선간호대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('cst.ac.kr','','조선이공대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('jhc.ac.kr','','진주보건대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('csc.ac.kr','','창신대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('cmu.ac.kr','','창원문성대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('yonam.ac.kr','','천안연암대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('chungkang.academy','','청강문화산업대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('scjc.ac.kr','','청암대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ch.ac.kr','','춘해보건대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('cyc.ac.kr','','충남도립청양대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('cpu.ac.kr','','충북도립대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('chsu.ac.kr','','충북보건과학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ok.ac.kr','','충청대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('pohang.ac.kr','','포항대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kg.ac.kr','','한국골프대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ktc.ac.kr','','한국관광대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('af.ac.kr','','한국농수산대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hanrw.ac.kr','','한국복지대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('corea.ac.kr','','한국복지사이버대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('klc.ac.kr','','한국승강기대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('pro.ac.kr','','한국영상대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('icpc.ac.kr','','한국정보통신기능대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('krc.ac.kr','','한국철도대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kopo.ac.kr ','','한국폴리텍'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hsc.ac.kr','','한림성심대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hywoman.ac.kr','','한양여자대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hanyeong.ac.kr','','한영대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hj.ac.kr','','혜전대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hu.ac.kr','','혜천대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kaya.ac.kr','','가야대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gachon.ac.kr','','가천대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gachon.ac.kr','','가천의과학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('catholic.ac.kr','','가톨릭대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('mtu.ac.kr','','감리교신학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kangnam.ac.kr','','강남대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gwnu.ac.kr','','강릉원주대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kangwon.ac.kr','','강원대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('konkuk.ac.kr','','건국대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kku.ac.kr','','건국대학교(글로컬)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('konyang.ac.kr','','건양대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kycu.ac.kr','','건양사이버대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kyonggi.ac.kr','','경기대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gntech.ac.kr','','경남과학기술대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hanma.kr','','경남대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('k1.ac.kr','','경동대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('knu.ac.kr','','경북대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kufs.ac.kr','','경북외국어대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gnu.ac.kr','','경상대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ks.ac.kr','','경성대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ikw.ac.kr','','경운대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ikw.ac.kr','','경운대학교(산업대)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ginue.ac.kr','','경인교육대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kiu.kr','','경일대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gju.ac.kr','','경주대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('khu.ac.kr','','경희대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('khcu.ac.kr','','경희사이버대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kmu.ac.kr','','계명대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('korea.ac.kr','','고려대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('korea.ac.kr','','고려대학교(세종)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('cuk.edu','','고려사이버대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kosin.ac.kr','','고신대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gjue.ac.kr','','공주교육대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('smail.kongju.ac.kr','','공주대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('cku.ac.kr','','가톨릭관동대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kwangshin.ac.kr','','광신대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kw.ac.kr','','광운대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kjcatholic.ac.kr','','광주가톨릭대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gist.ac.kr','','GIST'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gnue.ac.kr','','광주교육대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gwangju.ac.kr','','광주대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gwangju.ac.kr','','광주대학교(산업대)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kwu.ac.kr','','광주여자대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kookmin.ac.kr','','국민대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gcu.ac','','국제사이버대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kunsan.ac.kr','','군산대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kcu.ac.kr','','그리스도대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kdu.ac.kr','','극동대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('global.ac.kr','','글로벌사이버대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ggu.ac.kr','','금강대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kumoh.ac.kr','','금오공과대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gimcheon.ac.kr','','김천대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kkot.ac.kr','','꽃동네대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kornu.ac.kr','','나사렛대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('nambu.ac.kr','','남부대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('nsu.ac.kr','','남서울대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('nsu.ac.kr','','남서울대학교(산업대)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dankook.ac.kr','','단국대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('cu.ac.kr','','대구가톨릭대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dgist.ac.kr','','DGIST'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dnue.ac.kr','','대구교육대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('daegu.ac.kr','','대구대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dcu.ac.kr','','대구사이버대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dgau.ac.kr','','대구예술대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dufs.ac.kr','','대구외국어대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dhu.ac.kr','','대구한의대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('daeshin.ac.kr','','대신대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dcatholic.ac.kr','','대전가톨릭대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('edu.dju.ac.kr','','대전대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('daejeon.ac.kr','','대전신학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('daejeon.ac.kr','','대전신학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('daejin.ac.kr','','대진대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('duksung.ac.kr','','덕성여자대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dongguk.edu','','동국대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dongguk.ac.kr','','동국대학교(경주)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dongduk.ac.kr','','동덕여자대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('tu.ac.kr','','동명대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('tu.ac.kr','','동명정보대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dongseo.ac.kr','','동서대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dsu.kr','','동신대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('donga.ac.kr','','동아대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('dyu.ac.kr','','동양대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('deu.ac.kr','','동의대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('scau.ac.kr','','디지털서울문화예술대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ltu.ac.kr','','루터대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('mju.ac.kr','','명지대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('mokwon.ac.kr','','목원대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('mcu.ac.kr','','목포가톨릭대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('mokpo.ac.kr','','목포대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('mmu.ac.kr','','목포해양대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('pcu.ac.kr','','배재대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('bu.ac.kr','','백석대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('pukyong.ac.kr','','부경대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('cup.ac.kr','','부산가톨릭대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('bnue.ac.kr','','부산교육대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('pusan.ac.kr','','부산대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('bdu.ac.kr','','부산디지털대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('bufs.ac.kr','','부산외국어대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('bpu.ac.kr','','부산장신대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('cufs.ac.kr','','사이버한국외국어대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('syuin.ac.kr','','삼육대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sangmyung.kr','','상명대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sangmyung.kr','','상명대학교(천안)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('knu.ac.kr','','상주대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sangji.ac.kr','','상지대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sogang.ac.kr','','서강대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('skuniv.ac.kr','','서경대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('seonam.ac.kr','','서남대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('seoultech.ac.kr','','서울과학기술대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('seoultech.ac.kr','','서울과학기술대학교(산업대)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('snue.ac.kr','','서울교육대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('scu.ac.kr','','서울기독대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('snu.ac.kr','','서울대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sdu.ac.kr','','서울디지털대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('iscu.ac.kr','','서울사이버대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('uos.ac.kr','','서울시립대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('stu.ac.kr','','서울신학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('swu.ac.kr','','서울여자대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sjs.ac.kr','','서울장신대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('seowon.ac.kr','','서원대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sunmoon.ac.kr','','선문대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sungkyul.ac.kr','','성결대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('skhu.ac.kr','','성공회대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('skku.edu','','성균관대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sungshin.ac.kr','','성신여자대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('semyung.ac.kr','','세명대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sju.ac.kr','','세종대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sjcu.ac.kr','','세종사이버대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sehan.ac.kr','','세한대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('songwon.ac.kr','','송원대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('suwoncatholic.ac.kr','','수원가톨릭대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('suwon.ac.kr','','수원대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sookmyung.ac.kr','','숙명여자대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kcc.ac.kr','','순복음총회신학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('scnu.ac.kr','','순천대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sch.ac.kr','','순천향대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('soongsil.ac.kr','','숭실대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kcu.ac','','숭실사이버대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sgu.ac.kr','','신경대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('silla.ac.kr','','신라대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('acts.ac.kr','','아세아연합신학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ajou.ac.kr','','아주대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('anu.ac.kr','','안동대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ayum.anyang.ac.kr','','안양대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('yonsei.ac.kr','','연세대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('yonsei.ac.kr','','연세대학교(원주)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ocu.ac.kr','','열린사이버대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ynu.ac.kr','','영남대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ytus.ac.kr','','영남신학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('u1.ac.kr','','유원대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ysu.ac.kr','','영산대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ysu.ac.kr','','영산대학교(산업대)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('youngsan.ac.kr','','영산선학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('jesus.ac.kr','','예수대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('yewon.ac.kr','','예원예술대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('yiu.ac.kr','','용인대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('woosuk.ac.kr','','우석대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('wsu.ac.kr','','우송대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('wsu.ac.kr','','우송대학교(산업대)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('unist.ac.kr','','UNIST'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ulsan.ac.kr','','울산대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('wonkwang.ac.kr','','원광대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('wdu.ac.kr','','원광디지털대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('uu.ac.kr','','위덕대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('eulji.ac.kr','','을지대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ewhain.net','','이화여자대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('inje.ac.kr','','인제대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('iccu.ac.kr','','인천가톨릭대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('inu.ac.kr','','인천대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('inha.edu','','인하대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('pcts.ac.kr','','장로회신학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('jnu.ac.kr','','전남대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('jbnu.ac.kr','','전북대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('jnue.kr','','전주교육대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('jj.ac.kr','','전주대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('jit.ac.kr','','정석대학'); +INSERT INTO university (email_suffix, logo, name) VALUES ('jejue.ac.kr','','제주교육대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('jeju.ac.kr','','제주국제대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('jejunu.ac.kr','','제주대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('chosun.kr','','조선대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('jmail.ac.kr','','중부대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('cau.ac.kr','','중앙대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('cau.ac.kr','','중앙대학교(안성)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('sangha.ac.kr','','중앙승가대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('jwu.ac.kr','','중원대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('cue.ac.kr','','진주교육대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('gntech.ac.kr','','진주산업대학교(산업대)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('cha.ac.kr','','차의과학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('cs.ac.kr','','창신대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('changwon.ac.kr','','창원대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('chungwoon.ac.kr','','청운대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('cje.ac.kr','','청주교육대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('cju.ac.kr','','청주대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('chodang.ac.kr','','초당대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('chodang.ac.kr','','초당대학교(산업대)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('chongshin.ac.kr','','총신대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('chugye.ac.kr','','추계예술대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('cnue.ac.kr','','춘천교육대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('cnu.ac.kr','','충남대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('chungbuk.ac.kr','','충북대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kbtus.ac.kr','','침례신학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('calvin.ac.kr','','칼빈대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('tnu.ac.kr','','탐라대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ptu.ac.kr','','평택대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('postech.ac.kr','','POSTECH'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hknu.ac.kr','','한경대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hknu.ac.kr','','한경대학교(산업대)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kaist.ac.kr','','KAIST'); +INSERT INTO university (email_suffix, logo, name) VALUES ('knue.ac.kr','','한국교원대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ut.ac.kr','','한국교통대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('ut.ac.kr','','한국교통대학교(산업대)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('iuk.ac.kr','','한국국제대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('koreatech.ac.kr','','한국기술교육대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('knou.ac.kr','','한국방송통신대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kpu.ac.kr','','한국산업기술대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kpu.ac.kr','','한국산업기술대학교(산업대)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('bible.ac.kr','','한국성서대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('karts.ac.kr','','한국예술종합학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hufs.ac.kr','','한국외국어대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('nuch.ac.kr','','한국전통문화대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('knsu.ac.kr','','한국체육대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kau.kr','','한국항공대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('kmou.ac.kr','','한국해양대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hannam.ac.kr','','한남대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('handong.edu','','한동대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('halla.ac.kr','','한라대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hanlyo.ac.kr','','한려대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hanlyo.ac.kr','','한려대학교(산업대)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hallym.ac.kr','','한림대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hanmin.ac.kr','','한민학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hanbat.ac.kr','','한밭대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hanbat.ac.kr','','한밭대학교(산업대)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hanbuk.ac.kr','','한북대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hanseo.ac.kr','','한서대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hansung.ac.kr','','한성대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('uohs.ac.kr','','한세대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hs.ac.kr','','한신대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hanyang.ac.kr','','한양대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hanyang.ac.kr','','한양대학교(ERICA)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hycu.ac.kr','','한양사이버대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hytu.ac.kr','','한영신학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hanil.ac.kr','','한일장신대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hanzhong.ac.kr','','한중대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('uhs.ac.kr','','협성대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('honam.ac.kr','','호남대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('htus.ac.kr','','호남신학대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hoseo.edu','','호서대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('howon.ac.kr','','호원대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hongik.ac.kr','','홍익대학교'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hongik.ac.kr','','홍익대학교(세종)'); +INSERT INTO university (email_suffix, logo, name) VALUES ('hscu.ac.kr','','화신사이버대학교'); + +-- FREE Community 생성 +INSERT INTO community (deleted, department_id, title, description) +SELECT 0, id, 'FREE', '자유' +FROM department; + +-- GRADUATE Community 생성 +INSERT INTO community (deleted, department_id, title, description) +SELECT 0, id, 'GRADUATE', '대학원' +FROM department; + +-- JOB Community 생성 +INSERT INTO community (deleted, department_id, title, description) +SELECT 0, id, 'JOB', '취준' +FROM department; + +-- STUDY Community 생성 +INSERT INTO community (deleted, department_id, title, description) +SELECT 0, id, 'STUDY', '학업 및 스터디 구인' +FROM department; + +-- QUESTION Community 생성 +INSERT INTO community (deleted, department_id, title, description) +SELECT 0, id, 'QUESTION', '질문과 답변' +FROM department; + +-- PROMOTION Community 생성 +INSERT INTO community (deleted, department_id, title, description) +SELECT 0, id, 'PROMOTION', '홍보' +FROM department; \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V2__create_report_table.sql b/backend/src/main/resources/db/migration/V2__create_report_table.sql new file mode 100644 index 0000000000..3d605150f6 --- /dev/null +++ b/backend/src/main/resources/db/migration/V2__create_report_table.sql @@ -0,0 +1,10 @@ +create table report +( + created_date_time datetime(6) not null, + id bigint not null auto_increment, + reported_object_id bigint not null, + reporter_id bigint not null, + reason enum ('INSULTING','COMMERCIAL','INAPPROPRIATE','FRAUD','SPAM','PORNOGRAPHIC') not null, + report_type enum ('POST','COMMENT','REPLY') not null, + primary key (id) +) engine=InnoDB \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V3__create_blocklist_table.sql b/backend/src/main/resources/db/migration/V3__create_blocklist_table.sql new file mode 100644 index 0000000000..68bc06fd91 --- /dev/null +++ b/backend/src/main/resources/db/migration/V3__create_blocklist_table.sql @@ -0,0 +1,7 @@ +create table blocklist +( + id bigint not null auto_increment, + user_id bigint not null, + created_date_time datetime(6) not null, + primary key (id) +) engine = InnoDB; \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V4__create_userblock_table.sql b/backend/src/main/resources/db/migration/V4__create_userblock_table.sql new file mode 100644 index 0000000000..9b4cbcd062 --- /dev/null +++ b/backend/src/main/resources/db/migration/V4__create_userblock_table.sql @@ -0,0 +1,7 @@ +create table user_block +( + blocked_user_id bigint not null, + blocker_user_id bigint not null, + id bigint not null auto_increment, + primary key (id) +) engine=InnoDB \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V5__modify_post_content_varchar.sql b/backend/src/main/resources/db/migration/V5__modify_post_content_varchar.sql new file mode 100644 index 0000000000..3d4920c192 --- /dev/null +++ b/backend/src/main/resources/db/migration/V5__modify_post_content_varchar.sql @@ -0,0 +1 @@ +ALTER TABLE post MODIFY COLUMN content VARCHAR(2000) NOT NULL; \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V6__add_version_column.sql b/backend/src/main/resources/db/migration/V6__add_version_column.sql new file mode 100644 index 0000000000..8a001ded76 --- /dev/null +++ b/backend/src/main/resources/db/migration/V6__add_version_column.sql @@ -0,0 +1,2 @@ +ALTER TABLE post ADD COLUMN version INTEGER DEFAULT 0 NOT NULL; +ALTER TABLE comment ADD COLUMN version INTEGER DEFAULT 0 NOT NULL; \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V7__alter_version_column_to_bigint.sql b/backend/src/main/resources/db/migration/V7__alter_version_column_to_bigint.sql new file mode 100644 index 0000000000..996540ac33 --- /dev/null +++ b/backend/src/main/resources/db/migration/V7__alter_version_column_to_bigint.sql @@ -0,0 +1,2 @@ +ALTER TABLE comment MODIFY COLUMN version BIGINT; +ALTER TABLE post MODIFY COLUMN version BIGINT; diff --git a/backend/src/main/resources/templates/mail/email-authentication.html b/backend/src/main/resources/templates/mail/email-authentication.html new file mode 100644 index 0000000000..ffe17e8cf8 --- /dev/null +++ b/backend/src/main/resources/templates/mail/email-authentication.html @@ -0,0 +1,127 @@ + + + + email + + + + + + + + +
+ + + + + + + +
+ + + + + + + + + + +
+ 이메일 인증 코드 +
+ + + + +
+
+ 이메일 인증 코드가 발급되었습니다. +
+ 해당 문자를 인증 코드 입력창에 입력해 주세요. +
+
+
+ + \ No newline at end of file diff --git a/backend/src/main/resources/templates/mail/password-reset.html b/backend/src/main/resources/templates/mail/password-reset.html new file mode 100644 index 0000000000..49d2aa1734 --- /dev/null +++ b/backend/src/main/resources/templates/mail/password-reset.html @@ -0,0 +1,127 @@ + + + + email + + + + + + + + +
+ + + + + + + +
+ + + + + + + + + + +
+ 임시 비밀번호 +
+ + + + +
+
+ 임시 비밀번호가 발급되었습니다. +
+ 로그인 후 비밀번호를 변경해주세요. +
+
+
+ + \ No newline at end of file diff --git a/backend/src/test/kotlin/com/dclass/backend/BackendApplicationTests.kt b/backend/src/test/kotlin/com/dclass/backend/BackendApplicationTests.kt new file mode 100644 index 0000000000..857959ad13 --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/backend/BackendApplicationTests.kt @@ -0,0 +1,14 @@ +package com.dclass.backend + +import com.dclass.support.test.IntegrationTest +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest + +@IntegrationTest +class BackendApplicationTests { + + @Test + fun contextLoads() { + } + +} diff --git a/backend/src/test/kotlin/com/dclass/backend/application/BelongServiceTest.kt b/backend/src/test/kotlin/com/dclass/backend/application/BelongServiceTest.kt new file mode 100644 index 0000000000..f88b50ff89 --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/backend/application/BelongServiceTest.kt @@ -0,0 +1,106 @@ +package com.dclass.backend.application + +import com.dclass.backend.application.dto.UpdateDepartmentRequest +import com.dclass.backend.domain.belong.BelongRepository +import com.dclass.backend.domain.belong.getOrThrow +import com.dclass.backend.domain.department.DepartmentRepository +import com.dclass.backend.domain.department.getByIdOrThrow +import com.dclass.backend.domain.department.getByTitleOrThrow +import com.dclass.backend.exception.department.DepartmentException +import com.dclass.backend.exception.department.DepartmentExceptionType.NOT_FOUND_DEPARTMENT +import com.dclass.support.fixtures.belong +import com.dclass.support.fixtures.department +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import java.time.LocalDateTime +import java.util.* + +val UPDATEABLE = LocalDateTime.now().minusDays(100) +val NOT_UPDATEABLE = LocalDateTime.now() + +class BelongServiceTest : BehaviorSpec({ + + val departmentRepository = mockk() + val belongRepository = mockk() + + val belongService = BelongService(departmentRepository, belongRepository) + + Given("두 개의 학과가 존재하는 경우") { + val userId = 1L + val request = UpdateDepartmentRequest(major = "조선공학과", minor = "산업공학과") + + every { departmentRepository.getByTitleOrThrow(request.major) } returns department(id = 3L, title = request.major) + every { departmentRepository.getByTitleOrThrow(request.minor) } returns department(id = 4L, title = request.minor) + + + + When("특정 회원이 학과를 변경하면") { + val belong = belong(userId = userId, modifiedDateTime = UPDATEABLE) + every { belongRepository.getOrThrow(userId) } returns belong + val actual = belongService.editDepartments(userId, request) + + Then("회원의 학과가 변경된다") { + belong.major shouldBe 3L + belong.minor shouldBe 4L + } + } + + When("특정 회원의 학과 변경 가능 날짜를 조회하면") { + val belong = belong(userId = userId, modifiedDateTime = NOT_UPDATEABLE) + every { belongRepository.getOrThrow(userId) } returns belong + val actual = belongService.remain(userId) + + Then("남은 학과 변경 가능 날짜가 조회된다") { + actual.remainDays shouldBe 89 + } + } + } + + + + Given("학과가 존재하지 않는 경우") { + val userId = 1L + val request = UpdateDepartmentRequest(major = "존재하지않는 학과", minor = "산업공학과") + + every { departmentRepository.getByTitleOrThrow(request.major) } throws DepartmentException( + NOT_FOUND_DEPARTMENT + ) + every { departmentRepository.getByTitleOrThrow(request.minor) } returns department(id = 4L, title = request.minor) + + val belong = belong(userId = userId, modifiedDateTime = UPDATEABLE) + every { belongRepository.getOrThrow(userId) } returns belong + + When("특정 회원이 학과를 변경하면") { + Then("예외가 발생한다") { + shouldThrow { + belongService.editDepartments(userId, request) + }.exceptionType() shouldBe NOT_FOUND_DEPARTMENT + + } + } + } + + Given("특정 회원이 학과에 소속된 경우") { + val userId = 1L + + val belong = belong(userId = userId, modifiedDateTime = UPDATEABLE) + every { belongRepository.getOrThrow(userId) } returns belong + every { departmentRepository.findById(any()) } returns Optional.of(department()) + val department = departmentRepository.getByIdOrThrow(belong.activated) + + When("활성화된 학과를 변경하면") { + val actual = belongService.switchDepartment(userId) + + Then("활성화된 학과가 변경된다") { + belong.activated shouldBe 2L + belong.major shouldBe 1L + actual.activated shouldBe department.title + } + } + } + + +}) \ No newline at end of file diff --git a/backend/src/test/kotlin/com/dclass/backend/application/BlacklistServiceTest.kt b/backend/src/test/kotlin/com/dclass/backend/application/BlacklistServiceTest.kt new file mode 100644 index 0000000000..ba86b4db1d --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/backend/application/BlacklistServiceTest.kt @@ -0,0 +1,61 @@ +package com.dclass.backend.application + +import com.dclass.backend.domain.blacklist.Blacklist +import com.dclass.backend.domain.blacklist.BlacklistRepository +import com.dclass.backend.exception.blacklist.BlacklistException +import com.dclass.backend.exception.blacklist.BlacklistExceptionType.ALREADY_LOGOUT +import com.dclass.backend.security.JwtTokenProvider +import com.dclass.support.fixtures.blacklist +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.verify + +class BlacklistServiceTest : BehaviorSpec({ + + val blacklistRepository = mockk() + val jwtTokenProvider = mockk() + + val blacklistService = BlacklistService(blacklistRepository, jwtTokenProvider) + + Given("이미 사용자가 로그아웃한 경우") { + val token = "alreadyBlacklistedToken" + val alreadyBlacklistedToken: Blacklist = blacklist(invalidRefreshToken = token) + + justRun { jwtTokenProvider.validateToken(any()) } + every { blacklistRepository.findByInvalidRefreshToken(any()) } returns alreadyBlacklistedToken + + When("해당 토큰으로 재발급하면") { + Then("예외가 발생한다") { + shouldThrow { + blacklistService.reissueToken(token) + }.exceptionType() shouldBe ALREADY_LOGOUT + } + } + } + + Given("토큰이 유효하고 사용자가 로그아웃하지 않은 경우") { + val token = "valid" + + justRun { jwtTokenProvider.validateToken(any()) } + every { blacklistRepository.findByInvalidRefreshToken(any()) } returns null + every { blacklistRepository.save(any()) } returns blacklist() + every { jwtTokenProvider.getSubject(any()) } returns "email@naver.com" + every { jwtTokenProvider.createAccessToken(any()) } returns "accessToken" + every { jwtTokenProvider.createRefreshToken(any()) } returns "refreshToken" + + When("해당 토큰으로 재발급 하면") { + val response = blacklistService.reissueToken(token) + + Then("새로운 토큰을 발급한다") { + verify { blacklistRepository.save(any()) } + + response.accessToken shouldBe "accessToken" + response.refreshToken shouldBe "refreshToken" + } + } + } +}) diff --git a/backend/src/test/kotlin/com/dclass/backend/application/CommentIntegrationTest.kt b/backend/src/test/kotlin/com/dclass/backend/application/CommentIntegrationTest.kt new file mode 100644 index 0000000000..43d3fc66e6 --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/backend/application/CommentIntegrationTest.kt @@ -0,0 +1,128 @@ +package com.dclass.backend.application + +import com.dclass.backend.application.dto.* +import com.dclass.backend.domain.belong.BelongRepository +import com.dclass.backend.domain.comment.CommentRepository +import com.dclass.backend.domain.community.CommunityRepository +import com.dclass.backend.domain.post.PostRepository +import com.dclass.backend.domain.reply.ReplyRepository +import com.dclass.backend.domain.user.UniversityRepository +import com.dclass.backend.domain.user.UserRepository +import com.dclass.support.fixtures.* +import com.dclass.support.test.IntegrationTest +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.extensions.spring.SpringTestExtension +import io.kotest.extensions.spring.SpringTestLifecycleMode +import io.kotest.matchers.shouldBe + +@IntegrationTest +class CommentIntegrationTest( + private val commentService: CommentService, + private val commentRepository: CommentRepository, + private val replyRepository: ReplyRepository, + private val userRepository: UserRepository, + private val universityRepository: UniversityRepository, + private val replyService: ReplyService, + private val belongRepository: BelongRepository, + private val postRepository: PostRepository, + private val communityRepository: CommunityRepository, +) : BehaviorSpec({ + + extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) + + Given("하나의 게시글에 댓글과 대댓글이 있는 경우") { + + val univ = universityRepository.save(university()) + + repeat(10) { + userRepository.save(user(university = univ)) + belongRepository.save(belong(userId = it + 1L)) + postRepository.save(post()) + communityRepository.save(community()) + } + + val comments = commentRepository.saveAll( + listOf( + comment(1, 1, "저녁 메뉴 추천 받습니다"), + comment(1, 2, "게임 같이 하실 분"), + comment(1, 3, "내 대댓글은 어디 있을까?"), + ) + ) + + val replies = replyRepository.saveAll( + listOf( + reply(4, comments[0].id, "짜장면"), + reply(5, comments[0].id, "탕후루가 더 맛있어요"), + reply(4, comments[0].id, "글쎄 그건 간식 아냐?"), + reply(6, comments[1].id, "저도 같이 하고 싶어요"), + reply(7, comments[1].id, "무슨 게임 할거야?"), + ) + ) + + When("해당 게시글에 댓글을 작성하면") { + val comment = commentService.create(1, CreateCommentRequest(1, "저녁 메뉴 추천 받습니다")) + + Then("댓글이 작성된다") { + val savedComment = commentRepository.findById(comment.id).get() + savedComment.content shouldBe "저녁 메뉴 추천 받습니다" + } + } + + When("해당 게시글에 댓글을 삭제하면") { + val comment = commentService.create(1, CreateCommentRequest(1, "저녁 메뉴 추천 받습니다")) + commentService.delete(1, DeleteCommentRequest(comment.id)) + + Then("댓글이 삭제된다") { + val savedComment = commentRepository.findById(comment.id) + savedComment.isEmpty shouldBe true + } + } + + When("해당 게시글의 댓글과 대댓글을 조회하면") { + val comments = commentService.findAllByPostId(1, CommentScrollPageRequest(1L, null, 10)) + + Then("댓글과 대댓글이 조회된다") { + + comments.data.size shouldBe 4 + comments.data[0].replies.size shouldBe 3 + comments.data[1].replies.size shouldBe 2 + } + } + + When("해당 게시글의 댓글과 대댓글 수를 조회하면") { + val count = commentRepository.countCommentReplyByPostId(1L) + + Then("댓글과 대댓글 수가 조회된다") { + count shouldBe 9 + } + } + + When("해당 게시글의 댓글을 수정하면") { + commentService.update(1, UpdateCommentRequest(comments[0].id, "저녁 메뉴 추천 받아요")) + + Then("댓글이 수정된다") { + val comment = commentRepository.findCommentByIdAndUserId(comments[0].id, 1) + comment!!.content shouldBe "저녁 메뉴 추천 받아요" + } + } + + When("댓글에 좋아요를 누르면") { + commentService.like(2, LikeCommentRequest(comments[0].id)) + + Then("해당 댓글의 좋아요 수가 증가한다") { + val comment = commentRepository.findCommentByIdAndUserId(comments[0].id, 1) + comment!!.commentLikes.count shouldBe 1 + } + } + + When("대댓글에 좋아요를 누르면") { + replyService.like(1, LikeReplyRequest(replies[0].id)) + + Then("해당 대댓글의 좋아요 수가 증가한다") { + val reply = replyRepository.findByIdAndUserId(replies[0].id, 4) + reply!!.replyLikes.count shouldBe 1 + } + } + + } +}) \ No newline at end of file diff --git a/backend/src/test/kotlin/com/dclass/backend/application/NotificationIntegrationTest.kt b/backend/src/test/kotlin/com/dclass/backend/application/NotificationIntegrationTest.kt new file mode 100644 index 0000000000..4948488be0 --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/backend/application/NotificationIntegrationTest.kt @@ -0,0 +1,73 @@ +package com.dclass.backend.application + +import com.dclass.backend.application.dto.NotificationCommentRequest +import com.dclass.backend.domain.emitter.EmitterRepository +import com.dclass.backend.domain.notification.Notification +import com.dclass.backend.domain.notification.NotificationRepository +import com.dclass.backend.domain.notification.NotificationType +import com.dclass.backend.domain.notification.getOrThrow +import com.dclass.backend.domain.user.UniversityRepository +import com.dclass.backend.domain.user.UserRepository +import com.dclass.support.fixtures.university +import com.dclass.support.fixtures.user +import com.dclass.support.test.IntegrationTest +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.extensions.spring.SpringTestExtension +import io.kotest.extensions.spring.SpringTestLifecycleMode +import io.kotest.matchers.shouldBe + +private val DEFAULT_TIMEOUT = 60L * 1000L * 60L + +private fun makeTimeIncludeId(id: Long): String { + return "${id}_${System.currentTimeMillis()}" +} + +@IntegrationTest +class NotificationIntegrationTest( + private val notificationService: NotificationService, + private val notificationRepository: NotificationRepository, + private val emitterRepository: EmitterRepository, + private val userRepository: UserRepository, + private val universityRepository: UniversityRepository, +) : BehaviorSpec({ + + + extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) + + Given("특정 사용자가 알림을 구독하고 있는 경우") { + val univ = universityRepository.save(university()) + + val user = userRepository.save(user(university = univ)) + val emitter = notificationService.subscribe(user.id, "") + + val emitterId = makeTimeIncludeId(user.id) + emitterRepository.save(emitterId, emitter) + + When("해당 사용자의 이미터를 조회하면") { + val actual = emitterRepository.get(emitterId) + + Then("이미터가 조회된다") { + actual shouldBe emitter + } + } + + When("해당 사용자에게 알림을 전송하면") { + notificationService.send(NotificationCommentRequest(user.id, 1, 1, "알림내용", "FREE", NotificationType.COMMENT)) + + Then("알림이 전송된다") { + val notification = notificationRepository.findAll().first() + val event = emitterRepository.findAllEventCacheStartWithByUserId(user.id.toString()).values.first() + event shouldBe notification + } + } + + When("해당 사용자가 알림을 읽으면") { + val notification = notificationRepository.save(Notification(user.id, 1, "알림내용", type = NotificationType.COMMENT)) + notificationService.readNotification(notification.id) + + Then("알림이 읽힌다") { + notificationRepository.getOrThrow(notification.id).isRead shouldBe true + } + } + } +}) diff --git a/backend/src/test/kotlin/com/dclass/backend/application/PostIntegrationTest.kt b/backend/src/test/kotlin/com/dclass/backend/application/PostIntegrationTest.kt new file mode 100644 index 0000000000..106866b5be --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/backend/application/PostIntegrationTest.kt @@ -0,0 +1,279 @@ +package com.dclass.backend.application + +import com.dclass.backend.application.dto.DeletePostRequest +import com.dclass.backend.application.dto.PostDetailResponse +import com.dclass.backend.application.dto.PostScrollPageRequest +import com.dclass.backend.domain.belong.BelongRepository +import com.dclass.backend.domain.comment.CommentRepository +import com.dclass.backend.domain.community.CommunityRepository +import com.dclass.backend.domain.post.PostCount +import com.dclass.backend.domain.post.PostRepository +import com.dclass.backend.domain.scrap.ScrapRepository +import com.dclass.backend.domain.user.UniversityRepository +import com.dclass.backend.domain.user.UserRepository +import com.dclass.backend.exception.post.PostException +import com.dclass.backend.exception.post.PostExceptionType +import com.dclass.support.fixtures.* +import com.dclass.support.test.IntegrationTest +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.extensions.spring.SpringTestExtension +import io.kotest.extensions.spring.SpringTestLifecycleMode +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.instanceOf +import org.springframework.transaction.annotation.Transactional + +const val NEVER_EXIST_ID: Long = 999_999 + +@IntegrationTest +@Transactional +class PostIntegrationTest( + private val postService: PostService, + private val userRepository: UserRepository, + private val belongRepository: BelongRepository, + private val universityRepository: UniversityRepository, + private val communityRepository: CommunityRepository, + private val postRepository: PostRepository, + private val scrapRepository: ScrapRepository, + private val commentRepository: CommentRepository, +) : BehaviorSpec({ + extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) + + Given("특정 학과에 유저가 존재 할 경우") { + val univ = universityRepository.save(university()) + val user = userRepository.save(user(university = univ)) + val community = communityRepository.save(community(title = "JOB")) + belongRepository.save( + belong( + userId = user.id, + departmentIds = listOf(community.departmentId, community.departmentId + 1) + ) + ) + + When("게시글을 작성하면") { + val actual = postService.create( + user.id, + createPostRequest() + ) + + Then("게시글이 생성된다") { + actual shouldBe instanceOf() + } + } + + When("게시글을 삭제하면") { + val post = postRepository.save(post(userId = user.id, communityId = community.departmentId)) + postService.delete(user.id, DeletePostRequest(post.id)) + + Then("게시글이 삭제된다") { + shouldThrow { + postService.getById(user.id, post.id) + }.exceptionType() shouldBe PostExceptionType.NOT_FOUND_POST + } + } + + When("게시글을 수정하면") { + val post = postRepository.save(post(userId = user.id, communityId = community.departmentId)) + postService.update(user.id, updatePostRequest(postId = post.id)) + + Then("게시글이 수정된다") { + val actual = postService.getById(user.id, post.id) + actual.postTitle shouldBe "수정된 제목" + actual.postContent shouldBe "수정된 내용" + } + } + + When("존재하지 않는 게시글에 삭제 및 수정을 시도하면") { + Then("예외가 발생한다") { + shouldThrow { + postService.delete(user.id, DeletePostRequest(NEVER_EXIST_ID)) + }.exceptionType() shouldBe PostExceptionType.NOT_FOUND_POST + + shouldThrow { + postService.update(user.id, updatePostRequest(postId = NEVER_EXIST_ID)) + }.exceptionType() shouldBe PostExceptionType.NOT_FOUND_POST + } + } + + When("게시글에 좋아요를 누르면") { + val post = postRepository.save(post(userId = 2L, communityId = community.departmentId)) + + postService.likes(user.id, post.id) + + Then("게시글에 좋아요가 눌린다") { + val actual = postService.getById(user.id, post.id) + actual.count.likeCount shouldBe 1 + } + } + + } + + Given("특정 학과에 속한 학생이 게시글을 작성한 경우") { + val univ = universityRepository.save(university()) + val user = userRepository.save(user(university = univ)) + val community = communityRepository.save(community()) + val belong = + belongRepository.save( + belong( + userId = user.id, + departmentIds = listOf(community.departmentId) + ) + ) + val post = postRepository.save(post(userId = user.id, communityId = community.id)) + + When("게시글을 하나 조회하면") { + val actual = postService.getById(user.id, post.id) + + Then("게시글이 조회된다") { + actual.id shouldBe post.id + actual.userNickname shouldBe user.nickname + actual.universityName shouldBe user.universityName + actual.communityTitle shouldBe community.title + } + /** + * 테스트마다 비용이 부과되어 주석처리 + */ + Then("이미지가 조회된다") { + //actual.images.forEach { it shouldStartWith "https://dclass" } + } + } + + + val anotherUser = userRepository.save(user(university = univ)) + val notBelong = + belongRepository.save( + belong( + userId = anotherUser.id, + departmentIds = listOf(NEVER_EXIST_ID) + ) + ) + + When("다른 학과에 속한 학생이 게시글을 조회하면") { + Then("게시글이 조회되지 않는다") { + shouldThrow { + postService.getById(anotherUser.id, post.id) + }.exceptionType() shouldBe PostExceptionType.FORBIDDEN_POST + } + } + } + + Given("특정 학과에 속한 학생이 게시글을 조회하는 경우") { + val univ = universityRepository.save(university()) + val user = userRepository.save(user(university = univ)) + + val communities = communityRepository.saveAll( + listOf( + community(departmentId = 1L, title = "FREE"), + community(departmentId = 1L, title = "GRADUATE"), + community(departmentId = 1L, title = "JOB"), + ) + ) + belongRepository.save( + belong( + userId = user.id, + departmentIds = communities.map { it.departmentId }.distinct() + ) + ) + + repeat(5) { + postRepository.save( + post( + userId = user.id, + communityId = communities[0].id, + postCount = PostCount() + ) + ) + postRepository.save( + post( + userId = user.id, + communityId = communities[1].id, + postCount = PostCount() + ) + ) + postRepository.save( + post( + userId = user.id, + communityId = communities[2].id, + postCount = PostCount() + ) + ) + postRepository.save(post(title = "검색용 제목", content = "내용입니다", userId = user.id, communityId = communities[2].id)) + } + + val anotherUser = userRepository.save(user(university = univ)) + + val notMyCommunities = communityRepository.saveAll( + listOf( + community(departmentId = 2L, title = "다른 자유"), + community(departmentId = 2L, title = "다른 대학원"), + community(departmentId = 2L, title = "다른 취업"), + ) + ) + + repeat(5) { + postRepository.save( + post( + userId = anotherUser.id, + communityId = notMyCommunities[0].id, + postCount = PostCount() + ) + ) + postRepository.save( + post( + userId = anotherUser.id, + communityId = notMyCommunities[1].id, + postCount = PostCount() + ) + ) + postRepository.save( + post( + userId = anotherUser.id, + communityId = notMyCommunities[2].id, + postCount = PostCount() + ) + ) + postRepository.save(post(title = "검색용 제목", content = "내용입니다", userId = anotherUser.id, communityId = notMyCommunities[2].id)) + } + + When("모든 게시글을 조회하면") { + val actual = + postService.getAll(user.id, PostScrollPageRequest(size = 30, isHot = false)) + + Then("자신이 속한 학과 커뮤니티의 모든 게시글이 조회된다") { + actual.meta.count shouldBe 30 + } + } + + When("특정 유저가 작성한 게시글을 조회하면") { + val actual = postService.getByUserId(user.id, PostScrollPageRequest(size = 30)) + + Then("특정 유저가 작성한 게시글이 조회된다") { + actual.meta.count shouldBe 20 + } + } + + When("특정 유저가 특정 게시글을 스크랩하면") { + repeat(5) { + scrapRepository.save(scrap(userId = user.id, postId = it + 1L)) + } + + val actual = postService.getScrapped(user.id, PostScrollPageRequest(size = 30)) + + Then("특정 유저가 스크랩한 게시글이 조회된다") { + actual.meta.count shouldBe 5 + } + } + + When("특정 유저가 특정 게시글에 댓글을 작성하면") { + repeat(5) { + commentRepository.save(comment(userId = user.id, postId = it + 1L)) + } + + val actual = postService.getCommentedAndReplied(user.id, PostScrollPageRequest(size = 30)) + + Then("특정 유저가 댓글을 작성한 게시글이 조회된다") { + actual.meta.count shouldBe 5 + } + } + } +}) \ No newline at end of file diff --git a/backend/src/test/kotlin/com/dclass/backend/application/ReplyServiceTest.kt b/backend/src/test/kotlin/com/dclass/backend/application/ReplyServiceTest.kt new file mode 100644 index 0000000000..a6ea743e4e --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/backend/application/ReplyServiceTest.kt @@ -0,0 +1,199 @@ +package com.dclass.backend.application + +import com.dclass.backend.application.dto.CreateReplyRequest +import com.dclass.backend.application.dto.DeleteReplyRequest +import com.dclass.backend.application.dto.LikeReplyRequest +import com.dclass.backend.application.dto.UpdateReplyRequest +import com.dclass.backend.domain.comment.Comment +import com.dclass.backend.domain.comment.CommentRepository +import com.dclass.backend.domain.comment.getByIdOrThrow +import com.dclass.backend.domain.community.CommunityRepository +import com.dclass.backend.domain.community.findByIdOrThrow +import com.dclass.backend.domain.notification.NotificationEvent +import com.dclass.backend.domain.post.PostRepository +import com.dclass.backend.domain.post.findByIdOrThrow +import com.dclass.backend.domain.reply.ReplyRepository +import com.dclass.backend.domain.reply.getByIdAndUserIdOrThrow +import com.dclass.backend.domain.reply.getByIdOrThrow +import com.dclass.backend.exception.comment.CommentException +import com.dclass.backend.exception.comment.CommentExceptionType +import com.dclass.backend.exception.reply.ReplyException +import com.dclass.backend.exception.reply.ReplyExceptionType +import com.dclass.support.fixtures.comment +import com.dclass.support.fixtures.community +import com.dclass.support.fixtures.post +import com.dclass.support.fixtures.reply +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.ints.shouldBeLessThan +import io.kotest.matchers.shouldBe +import io.mockk.* +import org.springframework.context.ApplicationEventPublisher + +class ReplyServiceTest : BehaviorSpec({ + + val replyRepository = mockk() + val replyValidator = mockk() + val eventPublisher = mockk() + val communityRepository = mockk() + val commentRepository = mockk() + val postRepository = mockk() + + val replyService = ReplyService( + replyRepository, + replyValidator, + eventPublisher, + communityRepository, + commentRepository, + postRepository + ) + + Given("삭제된 댓글이 존재하는 경우") { + val comment = mockk() + every { commentRepository.getByIdOrThrow(any()) } returns comment + every { comment.isDeleted() } returns true + + When("답글을 달면") { + Then("삭제된 댓글 예외가 발생한다") { + shouldThrow { + replyService.create(1L, CreateReplyRequest(1L, "reply content")) + }.exceptionType() shouldBe CommentExceptionType.DELETED_COMMENT + } + } + } + + Given("댓글이 존재하는 경우") { + val comment = comment() + every { commentRepository.getByIdOrThrow(any()) } returns comment + + val post = post() + every { postRepository.findByIdOrThrow(any()) } returns post + val community = community() + every { communityRepository.findByIdOrThrow(any()) } returns community + + justRun { replyValidator.validate(any(), any()) } + + val reply = reply() + every { replyRepository.save(any()) } returns reply + + mockkObject(NotificationEvent.Companion) { + every { NotificationEvent.replyToPostUser(any(), any(), any(), any()) } returns mockk() + every { + NotificationEvent.replyToCommentUser( + any(), + any(), + any(), + any() + ) + } returns mockk() + } + + justRun { eventPublisher.publishEvent(any()) } + + When("다른 사람이 답글을 달면") { + replyService.create(2L, CreateReplyRequest(1L, "reply content")) + + Then("답글이 생성된다") { + verify { replyRepository.save(any()) } + } + + Then("댓글의 답글 수가 증가한다") { + comment.replyCount shouldBe 1 + } + + Then("답글 알림을 발송한다") { + verify { eventPublisher.publishEvent(any()) } + } + } + } + + Given("답글이 존재하는 경우") { + val reply = reply() + every { replyRepository.getByIdAndUserIdOrThrow(any(), any()) } returns reply + + When("답글을 수정하면") { + replyService.update(1L, UpdateReplyRequest(1L, "updated content")) + + Then("내용이 변경된다") { + reply.content shouldBe "updated content" + } + } + } + + Given("답글이 존재하는 경우2") { + val reply = reply() + every { replyRepository.getByIdAndUserIdOrThrow(any(), any()) } returns reply + + val comment = comment() + every { commentRepository.getByIdOrThrow(any()) } returns comment + + val post = post() + every { postRepository.findByIdOrThrow(any()) } returns post + + justRun { replyRepository.delete(any()) } + + When("답글을 삭제하면") { + replyService.delete(1L, DeleteReplyRequest(1L)) + + Then("답글이 삭제된다") { + verify { replyRepository.delete(reply) } + } + + Then("댓글의 답글 수가 감소한다") { + comment.replyCount shouldBeLessThan 0 + } + + Then("게시글의 댓글 답글 수가 감소한다") { + post.postCount.commentReplyCount shouldBe 29 + } + } + } + + Given("답글이 존재하는 경우3") { + val reply = reply() + every { replyRepository.getByIdOrThrow(any()) } returns reply + + val comment = comment() + every { commentRepository.getByIdOrThrow(any()) } returns comment + + val post = post() + every { postRepository.findByIdOrThrow(any()) } returns post + + val community = community() + every { communityRepository.findByIdOrThrow(any()) } returns community + + justRun { replyValidator.validate(any(), any()) } + + When("답글을 좋아요하면") { + replyService.like(2L, LikeReplyRequest(1L)) + + Then("답글의 좋아요 수가 증가한다") { + reply.likeCount shouldBe 1 + } + } + } + + Given("답글이 존재하는 경우4") { + val reply = reply() + every { replyRepository.getByIdOrThrow(any()) } returns reply + + val comment = comment() + every { commentRepository.getByIdOrThrow(any()) } returns comment + + val post = post() + every { postRepository.findByIdOrThrow(any()) } returns post + + val community = community() + every { communityRepository.findByIdOrThrow(any()) } returns community + + justRun { replyValidator.validate(any(), any()) } + + When("자기 답글에 좋아요하면") { + Then("예외가 발생한다") { + shouldThrow { + replyService.like(1L, LikeReplyRequest(1L)) + }.exceptionType() shouldBe ReplyExceptionType.SELF_LIKE + } + } + } +}) diff --git a/backend/src/test/kotlin/com/dclass/backend/application/ScrapServiceTest.kt b/backend/src/test/kotlin/com/dclass/backend/application/ScrapServiceTest.kt new file mode 100644 index 0000000000..93766918a0 --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/backend/application/ScrapServiceTest.kt @@ -0,0 +1,96 @@ +package com.dclass.backend.application + +import com.dclass.backend.application.dto.PostResponse +import com.dclass.backend.domain.post.PostRepository +import com.dclass.backend.domain.post.findByIdOrThrow +import com.dclass.backend.domain.scrap.ScrapRepository +import com.dclass.backend.exception.scrap.ScrapException +import com.dclass.backend.exception.scrap.ScrapExceptionType +import com.dclass.support.fixtures.post +import com.dclass.support.fixtures.scrap +import com.dclass.support.fixtures.user +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.ints.shouldBeLessThan +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.verify + +class ScrapServiceTest : BehaviorSpec({ + + val scrapRepository = mockk() + val postRepository = mockk() + val validator = mockk() + + val scrapService = ScrapService(scrapRepository, postRepository, validator) + + Given("특정 사용자가") { + val post = post() + + every { validator.validateScrapPost(any(), any()) } returns post + every { scrapRepository.save(any()) } returns scrap() + + When("스크랩을 생성하면") { + scrapService.create(1L, 1L) + + Then("save 메서드가 호출된다") { + verify(exactly = 1) { scrapRepository.save(scrap()) } + } + + Then("스크랩 갯수가 증가한다") { + post.postCount.scrapCount shouldBe 1 + } + } + } + + Given("특정 사용자가 게시글을 스크랩을 안했을 경우") { + val scrap = scrap() + + every { scrapRepository.findByUserIdAndPostId(any(), any()) } throws ScrapException( + ScrapExceptionType.NOT_FOUND_SCRAP + ) + + When("스크랩을 삭제하면") { + Then("예외가 발생한다") { + shouldThrow { + scrapService.delete(1L, 1L) + }.exceptionType() shouldBe ScrapExceptionType.NOT_FOUND_SCRAP + } + } + } + + Given("특정 사용자가 게시글을 스크랩을 했을 경우") { + val scrap = scrap() + val post = post() + + every { scrapRepository.findByUserIdAndPostId(any(), any()) } returns scrap + every { postRepository.findByIdOrThrow(any()) } returns post + justRun { scrapRepository.delete(scrap) } + + When("스크랩을 삭제하면") { + scrapService.delete(1L, 1L) + + Then("delete 메서드가 호출된다") { + verify(exactly = 1) { scrapRepository.delete(scrap) } + } + + Then("스크랩 갯수가 감소한다") { + post.postCount.scrapCount shouldBeLessThan 0 + } + } + } + + Given("특정 사용자가 스크랩한 게시글을 조회하면") { + every { postRepository.findScrapPostByUserId(any()) } returns listOf(PostResponse(post(),user(),"FREE")) + + When("스크랩한 게시글을 조회하면") { + val actual = scrapService.getAll(1L) + + Then("스크랩한 게시글이 조회된다") { + actual.size shouldBe 1 + } + } + } +}) diff --git a/backend/src/test/kotlin/com/dclass/backend/application/UserServiceTest.kt b/backend/src/test/kotlin/com/dclass/backend/application/UserServiceTest.kt new file mode 100644 index 0000000000..e2e74dde37 --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/backend/application/UserServiceTest.kt @@ -0,0 +1,160 @@ +package com.dclass.backend.application + +import com.dclass.backend.application.dto.* +import com.dclass.backend.domain.belong.Belong +import com.dclass.backend.domain.belong.BelongRepository +import com.dclass.backend.domain.belong.getOrThrow +import com.dclass.backend.domain.department.DepartmentRepository +import com.dclass.backend.domain.user.Password +import com.dclass.backend.domain.user.UserRepository +import com.dclass.backend.domain.user.findByEmail +import com.dclass.backend.domain.user.getOrThrow +import com.dclass.backend.exception.user.UserException +import com.dclass.backend.exception.user.UserExceptionType +import com.dclass.support.fixtures.* +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import org.springframework.data.repository.findByIdOrNull + +class UserServiceTest : BehaviorSpec({ + val userRepository = mockk() + val passwordGenerator = mockk() + val departmentRepository = mockk() + val belongRepository = mockk() + + val userService = + UserService(userRepository, passwordGenerator, departmentRepository, belongRepository) + + Given("특정 회원의 개인정보가 있는 경우") { + val user = user() + + every { userRepository.findByEmail(any()) } returns user + every { passwordGenerator.generate() } returns RANDOM_PASSWORD_TEXT + every { userRepository.save(any()) } returns user + every { userRepository.getOrThrow(any()) } returns user + + When("동일한 개인정보로 비밀번호를 초기화하면") { + userService.resetPassword(ResetPasswordRequest(user.name, user.email)) + + Then("비밀번호가 초기화된다") { + user.password shouldBe Password(RANDOM_PASSWORD_TEXT) + } + } + + When("일치하지 않는 개인정보로 비밀번호를 초기화하면") { + Then("예외가 발생한다") { + shouldThrow { + userService.resetPassword(ResetPasswordRequest("가짜 이름", user.email)) + }.exceptionType() shouldBe UserExceptionType.INVALID_USER_INFORMATION + } + } + + When("닉네임을 변경하면") { + val nickname = "변경된 닉네임" + userService.editNickname(user.id, UpdateNicknameRequest(nickname)) + + Then("닉네임이 변경된다") { + user.nickname shouldBe nickname + } + } + } + + Given("특정 회원이 있고 변경할 비밀번호가 있는 경우") { + val user = user(id = 1L, password = PASSWORD.value) + val password = NEW_PASSWORD + + every { userRepository.getOrThrow(any()) } returns user + + When("기존 비밀번호와 함께 새 비밀번호를 변경하면") { + userService.editPassword( + user.id, + EditPasswordRequest(user.password, password, password) + ) + + Then("새 비밀번호로 변경된다") { + user.password shouldBe password + } + } + + When("일치하지 않는 기존 비밀번호와 함께 새 비밀번호를 변경하면") { + Then("예외가 발생한다") { + shouldThrow { + userService.editPassword( + user.id, + EditPasswordRequest(WRONG_PASSWORD, password, password) + ) + }.exceptionType() shouldBe UserExceptionType.INVALID_PASSWORD_ACCESS_DENIED + } + } + + When("이전 비밀번호는 일치하지만 새 비밀번호와 확인 비밀번호가 일치하지 않으면") { + Then("예외가 발생한다") { + shouldThrow { + userService.editPassword( + user.id, + EditPasswordRequest(user.password, password, WRONG_PASSWORD) + ) + } + } + } + } + + Given("특정 회원이 학과에 속한 경우") { + val user = user(id = 1L) + val department = department() + val department2 = department(2L, "경영학과") + + every { userRepository.findUserInfoWithDepartment(any()) } returns UserResponseWithDepartment( + UserResponse(user), + listOf(department.id, department2.id), + ) + every { belongRepository.getOrThrow(any()) } returns Belong( + user.id, + listOf(department.id, department2.id), + ) + every { departmentRepository.findAllById(any()) } returns listOf(department, department2) + + When("해당 회원의 정보를 조회하면") { + val actual = userService.getInformation(user.id) + val belong = belongRepository.getOrThrow(user.id) + + Then("회원 정보를 확인할 수 있다") { + actual shouldBe UserResponseWithDepartmentNames( + userRepository.findByIdOrNull(user.id)!!, + department.title, + department2.title, + department.title, + ) + belong.activated shouldBe department.id + actual.major shouldBe department.title + actual.minor shouldBe department2.title + } + } + } + + Given("해당 회원이 선택한 전공이 하나인 경우엔") { + val user = user(id = 1L) + val department = department() + val department2 = department(id = 999, title = "") + + every { belongRepository.getOrThrow(any()) } returns Belong( + user.id, + listOf(department.id, department2.id), + ) + every { departmentRepository.findAllById(any()) } returns listOf(department, department2) + + When("해당 회원의 정보를 조회하면") { + val belong = belongRepository.getOrThrow(user.id) + + val actual = userService.getInformation(user.id) + + Then("부전공은 존재하지 않는다") { + actual.major shouldBe department.title + actual.minor shouldBe "" + } + } + } +}) \ No newline at end of file diff --git a/backend/src/test/kotlin/com/dclass/backend/domain/comment/CommentRepositorySupportImplTest.kt b/backend/src/test/kotlin/com/dclass/backend/domain/comment/CommentRepositorySupportImplTest.kt new file mode 100644 index 0000000000..fb0947defc --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/backend/domain/comment/CommentRepositorySupportImplTest.kt @@ -0,0 +1,64 @@ +package com.dclass.backend.domain.comment + +import com.dclass.backend.application.dto.CommentScrollPageRequest +import com.dclass.backend.application.dto.CommentWithUserResponse +import com.dclass.backend.domain.user.UniversityRepository +import com.dclass.backend.domain.user.UserRepository +import com.dclass.support.fixtures.comment +import com.dclass.support.fixtures.university +import com.dclass.support.fixtures.user +import com.dclass.support.test.RepositoryTest +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.extensions.spring.SpringTestExtension +import io.kotest.extensions.spring.SpringTestLifecycleMode +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldHaveSize + +@RepositoryTest +class CommentRepositorySupportImplTest( + private val userRepository: UserRepository, + private val commentRepository: CommentRepository, + private val universityRepository: UniversityRepository +) : BehaviorSpec({ + extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) + + Given("게시글에 두명의 사용자가 댓글을 작성할 때") { + val uni = universityRepository.save(university()) + val user1 = userRepository.save(user(university = uni)) + val user2 = userRepository.save(user(university = uni)) + + val comments1 = commentRepository.saveAll( + listOf( + comment(userId = user1.id, postId = 1L), + comment(userId = user1.id, postId = 1L), + comment(userId = user1.id, postId = 1L), + comment(userId = user1.id, postId = 1L), + comment(userId = user1.id, postId = 1L), + ) + ) + + val comments2 = commentRepository.saveAll( + listOf( + comment(userId = user2.id, postId = 1L), + comment(userId = user2.id, postId = 1L), + comment(userId = user2.id, postId = 1L), + comment(userId = user2.id, postId = 1L), + comment(userId = user2.id, postId = 1L), + ) + ) + + When("게시글에 해당하는 댓글을 조회하면") { + val comments = commentRepository.findCommentWithUserByPostId(CommentScrollPageRequest(postId = 1L)) + + Then("댓글 및 작성자 정보가 반환된다") { + + comments shouldHaveSize 10 + + val expected = comments1.map { + CommentWithUserResponse(it, user1) + } + comments2.map { CommentWithUserResponse(it, user2) } + comments shouldContainExactly expected + } + } + } +}) \ No newline at end of file diff --git a/backend/src/test/kotlin/com/dclass/backend/domain/post/PostRepositorySupportTest.kt b/backend/src/test/kotlin/com/dclass/backend/domain/post/PostRepositorySupportTest.kt new file mode 100644 index 0000000000..cb86adcdd6 --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/backend/domain/post/PostRepositorySupportTest.kt @@ -0,0 +1,42 @@ +package com.dclass.backend.domain.post + +import com.dclass.backend.domain.community.CommunityRepository +import com.dclass.backend.domain.user.UniversityRepository +import com.dclass.backend.domain.user.UserRepository +import com.dclass.support.fixtures.community +import com.dclass.support.fixtures.post +import com.dclass.support.fixtures.university +import com.dclass.support.fixtures.user +import com.dclass.support.test.RepositoryTest +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.extensions.spring.SpringTestExtension +import io.kotest.extensions.spring.SpringTestLifecycleMode +import io.kotest.matchers.shouldBe + +@RepositoryTest +class PostRepositorySupportTest( + private val userRepository: UserRepository, + private val postRepository: PostRepository, + private val communityRepository: CommunityRepository, + private val universityRepository: UniversityRepository +) : BehaviorSpec({ + extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) + + Given("게시글이 작성되어있는 경우") { + val univ = universityRepository.save(university()) + val user = userRepository.save(user(university = univ)) + val community = communityRepository.save(community()) + val post = postRepository.save(post(userId = user.id, communityId = community.id)) + + When("게시글을 조회하면") { + val actual = postRepository.findPostById(post.id) + + Then("사용자 정보 및 커뮤니티 정보를 포함한 게시글이 반환된다") { + actual.id shouldBe post.id + actual.userNickname shouldBe user.nickname + actual.universityName shouldBe user.universityName + actual.communityTitle shouldBe community.title + } + } + } +}) diff --git a/backend/src/test/kotlin/com/dclass/backend/domain/post/PostRepositoryTest.kt b/backend/src/test/kotlin/com/dclass/backend/domain/post/PostRepositoryTest.kt new file mode 100644 index 0000000000..12303564c0 --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/backend/domain/post/PostRepositoryTest.kt @@ -0,0 +1,50 @@ +package com.dclass.backend.domain.post + +import com.dclass.backend.application.dto.PostScrollPageRequest +import com.dclass.support.fixtures.post +import com.dclass.support.test.RepositoryTest +import io.kotest.core.spec.style.ExpectSpec +import io.kotest.extensions.spring.SpringTestExtension +import io.kotest.extensions.spring.SpringTestLifecycleMode +import io.kotest.matchers.longs.shouldBeGreaterThan +import io.kotest.matchers.shouldBe + +@RepositoryTest +class PostRepositoryTest( + private val postRepository: PostRepository +) : ExpectSpec({ + extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) + + context("게시글 조회") { + repeat(20) { + postRepository.save(post(title = "title $it", content = "content $it")) + } + + expect("lastId가 없으면 최신 게시글부터 조회한다") { + val postScrollPageRequest = PostScrollPageRequest( + lastId = null, + communityTitle = null, + size = 10, + ) + val test = postRepository.findAll() + val actual = postRepository.findPostScrollPage(postScrollPageRequest) + print(test) + actual.size shouldBe 10 + actual.first().id shouldBeGreaterThan actual.last().id + } + + val lastId = postRepository.findAll().last().id + + expect("lastId가 있으면 lastId보다 작은 게시글부터 조회한다") { + val postScrollPageRequest = PostScrollPageRequest( + lastId = lastId, + communityTitle = null, + size = 10, + ) + val actual = postRepository.findPostScrollPage(postScrollPageRequest) + + actual.size shouldBe 10 + actual.first().id shouldBeGreaterThan actual.last().id + } + } +}) \ No newline at end of file diff --git a/backend/src/test/kotlin/com/dclass/backend/domain/user/UserRepositoryTest.kt b/backend/src/test/kotlin/com/dclass/backend/domain/user/UserRepositoryTest.kt new file mode 100644 index 0000000000..1097d2aada --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/backend/domain/user/UserRepositoryTest.kt @@ -0,0 +1,37 @@ +package com.dclass.backend.domain.user + +import com.dclass.backend.domain.belong.BelongRepository +import com.dclass.support.fixtures.belong +import com.dclass.support.fixtures.university +import com.dclass.support.fixtures.user +import com.dclass.support.test.RepositoryTest +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.extensions.spring.SpringTestExtension +import io.kotest.extensions.spring.SpringTestLifecycleMode +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldHaveSize + +@RepositoryTest +class UserRepositoryTest( + private val userRepository: UserRepository, + private val belongRepository: BelongRepository, + private val universityRepository: UniversityRepository, +) : BehaviorSpec({ + extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) + + Given("특정 사용자가 소속된 학과가 있는 경우") { + val univ = universityRepository.save(university()) + val user = userRepository.save(user(university = univ)) + val belong = belongRepository.save(belong(userId = user.id)) + + When("사용자의 정보를 조회하면") { + val findInfo = userRepository.findUserInfoWithDepartment(user.id) + + Then("사용자의 정보와 소속된 학과 정보가 반환된다") { + findInfo.departmentIds shouldHaveSize 2 + findInfo.departmentIds shouldContainExactly belong.departmentIds + } + } + + } +}) diff --git a/backend/src/test/kotlin/com/dclass/support/fixtures/BelongFixtures.kt b/backend/src/test/kotlin/com/dclass/support/fixtures/BelongFixtures.kt new file mode 100644 index 0000000000..7aa6275c93 --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/support/fixtures/BelongFixtures.kt @@ -0,0 +1,16 @@ +package com.dclass.support.fixtures + +import com.dclass.backend.domain.belong.Belong +import java.time.LocalDateTime + +fun belong( + userId: Long = 1L, + departmentIds: List = listOf(1L, 2L), + modifiedDateTime: LocalDateTime = LocalDateTime.now(), +): Belong { + return Belong( + userId = userId, + ids = departmentIds, + modifiedDateTime = modifiedDateTime, + ) +} diff --git a/backend/src/test/kotlin/com/dclass/support/fixtures/BlacklistFixtures.kt b/backend/src/test/kotlin/com/dclass/support/fixtures/BlacklistFixtures.kt new file mode 100644 index 0000000000..9628ce4a85 --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/support/fixtures/BlacklistFixtures.kt @@ -0,0 +1,13 @@ +package com.dclass.support.fixtures + +import com.dclass.backend.domain.blacklist.Blacklist + +fun blacklist( + id: Long = 1L, + invalidRefreshToken: String = "invalid", +): Blacklist { + return Blacklist( + id = id, + invalidRefreshToken = invalidRefreshToken, + ) +} diff --git a/backend/src/test/kotlin/com/dclass/support/fixtures/CommentFixtures.kt b/backend/src/test/kotlin/com/dclass/support/fixtures/CommentFixtures.kt new file mode 100644 index 0000000000..3647f7bba0 --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/support/fixtures/CommentFixtures.kt @@ -0,0 +1,15 @@ +package com.dclass.support.fixtures + +import com.dclass.backend.domain.comment.Comment + +fun comment( + postId: Long = 1L, + userId: Long = 1L, + content: String = "comment content" +): Comment { + return Comment( + postId = postId, + userId = userId, + content = content + ) +} \ No newline at end of file diff --git a/backend/src/test/kotlin/com/dclass/support/fixtures/CommunityFixtures.kt b/backend/src/test/kotlin/com/dclass/support/fixtures/CommunityFixtures.kt new file mode 100644 index 0000000000..3f8a202769 --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/support/fixtures/CommunityFixtures.kt @@ -0,0 +1,15 @@ +package com.dclass.support.fixtures + +import com.dclass.backend.domain.community.Community + +fun community( + departmentId: Long = 1L, + title: String = "FREE", + description: String = "자유롭게 이야기할 수 있는 게시판입니다" +): Community { + return Community( + departmentId = departmentId, + title = title, + description = description, + ) +} \ No newline at end of file diff --git a/backend/src/test/kotlin/com/dclass/support/fixtures/DepartmentFixtures.kt b/backend/src/test/kotlin/com/dclass/support/fixtures/DepartmentFixtures.kt new file mode 100644 index 0000000000..86ba22e995 --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/support/fixtures/DepartmentFixtures.kt @@ -0,0 +1,13 @@ +package com.dclass.support.fixtures + +import com.dclass.backend.domain.department.Department + +fun department( + id: Long = 1L, + title: String = "컴퓨터공학과", +): Department { + return Department( + id = id, + title = title, + ) +} \ No newline at end of file diff --git a/backend/src/test/kotlin/com/dclass/support/fixtures/ImageFixtures.kt b/backend/src/test/kotlin/com/dclass/support/fixtures/ImageFixtures.kt new file mode 100644 index 0000000000..42ecccdbd0 --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/support/fixtures/ImageFixtures.kt @@ -0,0 +1,11 @@ +package com.dclass.support.fixtures + +import com.dclass.support.domain.Image + +fun createImage( + key: String +): Image { + return Image( + imageKey = key + ) +} \ No newline at end of file diff --git a/backend/src/test/kotlin/com/dclass/support/fixtures/PostFixtures.kt b/backend/src/test/kotlin/com/dclass/support/fixtures/PostFixtures.kt new file mode 100644 index 0000000000..fc0d971e57 --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/support/fixtures/PostFixtures.kt @@ -0,0 +1,73 @@ +package com.dclass.support.fixtures + +import com.dclass.backend.application.dto.CreatePostRequest +import com.dclass.backend.application.dto.UpdatePostRequest +import com.dclass.backend.domain.post.Post +import com.dclass.backend.domain.post.PostCount +import com.dclass.support.domain.Image + +fun post( + title: String = "title", + content: String = "content", + userId: Long = 1L, + communityId: Long = 1L, + images: List = listOf( + createImage("post/s3-test1"), + createImage("post/s3-test2"), + createImage("post/s3-test3"), + ), + postCount: PostCount = postCount(), +): Post { + return Post( + title = title, + content = content, + userId = userId, + communityId = communityId, + images = images, + postCount = postCount + ) +} + +fun postCount( + replyCount: Int = 30, + likeCount: Int = 15, + scrapCount: Int = 0, +): PostCount { + return PostCount( + commentReplyCount = replyCount, + likeCount = likeCount, + scrapCount = scrapCount + ) +} + +fun createPostRequest( + communityTitle: String = "JOB", + title: String = "title", + content: String = "content", + isQuestion: Boolean = false, + images: List = listOf(), +): CreatePostRequest { + return CreatePostRequest( + communityTitle = communityTitle, + title = title, + content = content, + isQuestion = isQuestion, + images = images + ) +} + +fun updatePostRequest( + postId: Long = 1L, + title: String = "수정된 제목", + content: String = "수정된 내용", + images: List = listOf(), + communityTitle: String = "JOB", +): UpdatePostRequest { + return UpdatePostRequest( + postId = postId, + title = title, + content = content, + images = images, + communityTitle = communityTitle + ) +} diff --git a/backend/src/test/kotlin/com/dclass/support/fixtures/ReplyFixtures.kt b/backend/src/test/kotlin/com/dclass/support/fixtures/ReplyFixtures.kt new file mode 100644 index 0000000000..6f1418ebfb --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/support/fixtures/ReplyFixtures.kt @@ -0,0 +1,15 @@ +package com.dclass.support.fixtures + +import com.dclass.backend.domain.reply.Reply + +fun reply( + userId: Long = 1L, + commentId: Long = 1L, + content: String = "reply content" +): Reply { + return Reply( + userId = userId, + commentId = commentId, + content = content + ) +} \ No newline at end of file diff --git a/backend/src/test/kotlin/com/dclass/support/fixtures/ScrapFIxtures.kt b/backend/src/test/kotlin/com/dclass/support/fixtures/ScrapFIxtures.kt new file mode 100644 index 0000000000..579f600b75 --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/support/fixtures/ScrapFIxtures.kt @@ -0,0 +1,13 @@ +package com.dclass.support.fixtures + +import com.dclass.backend.domain.scrap.Scrap + +fun scrap( + userId: Long = 1L, + postId: Long = 1L, +): Scrap { + return Scrap( + userId = userId, + postId = postId, + ) +} diff --git a/backend/src/test/kotlin/com/dclass/support/fixtures/UniversityFixtures.kt b/backend/src/test/kotlin/com/dclass/support/fixtures/UniversityFixtures.kt new file mode 100644 index 0000000000..141632edc8 --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/support/fixtures/UniversityFixtures.kt @@ -0,0 +1,17 @@ +package com.dclass.support.fixtures + +import com.dclass.backend.domain.user.University + +fun university( + id: Long = 1L, + name: String = "국민대학교", + emailSuffix: String = "kookmin.ac.kr", + logo: String = "logo" +): University { + return University( + id = id, + name = name, + emailSuffix = emailSuffix, + logo = logo + ) +} \ No newline at end of file diff --git a/backend/src/test/kotlin/com/dclass/support/fixtures/UserFixtures.kt b/backend/src/test/kotlin/com/dclass/support/fixtures/UserFixtures.kt new file mode 100644 index 0000000000..824f056cb8 --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/support/fixtures/UserFixtures.kt @@ -0,0 +1,32 @@ +package com.dclass.support.fixtures + +import com.dclass.backend.domain.user.Password +import com.dclass.backend.domain.user.University +import com.dclass.backend.domain.user.User +import java.util.* + +val usernames = listOf("devbelly", "zkxmdkdltm", "hongbuly", "jia5232") + +const val RANDOM_PASSWORD_TEXT: String = "nEw_p@ssw0rd" +val PASSWORD: Password = Password("password") +val NEW_PASSWORD: Password = Password("new_password") +val WRONG_PASSWORD: Password = Password("wrong_password") + + +fun user( + id: Long = 0L, + name: String = "username", + email: String = UUID.randomUUID().toString().take(6) + "@kookmin.ac.kr", + nickname: String = usernames.random(), + password: String = "password", + university: University = university() +): User { + return User( + id = id, + name = name, + email = email, + password = password, + nickname = nickname, + university = university + ) +} \ No newline at end of file diff --git a/backend/src/test/kotlin/com/dclass/support/test/BaseTests.kt b/backend/src/test/kotlin/com/dclass/support/test/BaseTests.kt new file mode 100644 index 0000000000..cebb008faf --- /dev/null +++ b/backend/src/test/kotlin/com/dclass/support/test/BaseTests.kt @@ -0,0 +1,35 @@ +package com.dclass.support.test + +import com.linecorp.kotlinjdsl.support.spring.data.jpa.autoconfigure.KotlinJdslAutoConfiguration +import io.mockk.junit5.MockKExtension +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Import +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.TestConstructor + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@ActiveProfiles("test") +@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL) +annotation class TestEnvironment + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@ExtendWith(MockKExtension::class) +annotation class UnitTest + + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@Import(KotlinJdslAutoConfiguration::class) +@DataJpaTest +@TestEnvironment +annotation class RepositoryTest + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@SpringBootTest +@TestEnvironment +annotation class IntegrationTest \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000000..625fbabf0a --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,47 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# ip list +lib/common/const/ip_list.dart \ No newline at end of file diff --git a/frontend/.metadata b/frontend/.metadata new file mode 100644 index 0000000000..d2765fcbec --- /dev/null +++ b/frontend/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "54e66469a933b60ddf175f858f82eaeb97e48c8d" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: android + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: ios + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: linux + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: macos + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: web + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: windows + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000000..6463ba2ded --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,16 @@ +# frontend + +2024 capstone + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/frontend/analysis_options.yaml b/frontend/analysis_options.yaml new file mode 100644 index 0000000000..0d2902135c --- /dev/null +++ b/frontend/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/frontend/android/.gitignore b/frontend/android/.gitignore new file mode 100644 index 0000000000..6f568019d3 --- /dev/null +++ b/frontend/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/frontend/android/app/build.gradle b/frontend/android/app/build.gradle new file mode 100644 index 0000000000..50120f1a86 --- /dev/null +++ b/frontend/android/app/build.gradle @@ -0,0 +1,80 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('app/key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + +android { + namespace "com.capstone.frontend" + compileSdk flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.capstone.decl" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile file(keystoreProperties['storeFile']) + storePassword keystoreProperties['storePassword'] + } + } + + buildTypes { + release { + signingConfig signingConfigs.release + } + } +} + +flutter { + source '../..' +} + +dependencies {} diff --git a/frontend/android/app/src/debug/AndroidManifest.xml b/frontend/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000000..83c512c97d --- /dev/null +++ b/frontend/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/frontend/android/app/src/main/AndroidManifest.xml b/frontend/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..cd05e28f12 --- /dev/null +++ b/frontend/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/android/app/src/main/kotlin/com/capstone/frontend/MainActivity.kt b/frontend/android/app/src/main/kotlin/com/capstone/frontend/MainActivity.kt new file mode 100644 index 0000000000..8c3241f506 --- /dev/null +++ b/frontend/android/app/src/main/kotlin/com/capstone/frontend/MainActivity.kt @@ -0,0 +1,5 @@ +package com.capstone.frontend + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/frontend/android/app/src/main/res/drawable-v21/launch_background.xml b/frontend/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000000..f74085f3f6 --- /dev/null +++ b/frontend/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/frontend/android/app/src/main/res/drawable/launch_background.xml b/frontend/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000000..304732f884 --- /dev/null +++ b/frontend/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..8fba0cccd4 Binary files /dev/null and b/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..cc0bdcc03a Binary files /dev/null and b/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..c15d44cbdb Binary files /dev/null and b/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..d483546fcf Binary files /dev/null and b/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..974434bf63 Binary files /dev/null and b/frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/frontend/android/app/src/main/res/values-night/styles.xml b/frontend/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000000..06952be745 --- /dev/null +++ b/frontend/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/frontend/android/app/src/main/res/values/styles.xml b/frontend/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000000..cb1ef88056 --- /dev/null +++ b/frontend/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/frontend/android/app/src/profile/AndroidManifest.xml b/frontend/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000000..399f6981d5 --- /dev/null +++ b/frontend/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/frontend/android/build.gradle b/frontend/android/build.gradle new file mode 100644 index 0000000000..bc157bd1a1 --- /dev/null +++ b/frontend/android/build.gradle @@ -0,0 +1,18 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/frontend/android/gradle.properties b/frontend/android/gradle.properties new file mode 100644 index 0000000000..598d13fee4 --- /dev/null +++ b/frontend/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G +android.useAndroidX=true +android.enableJetifier=true diff --git a/frontend/android/gradle/wrapper/gradle-wrapper.properties b/frontend/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..e1ca574ef0 --- /dev/null +++ b/frontend/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip diff --git a/frontend/android/settings.gradle b/frontend/android/settings.gradle new file mode 100644 index 0000000000..1d6d19b7f8 --- /dev/null +++ b/frontend/android/settings.gradle @@ -0,0 +1,26 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() + + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false + id "org.jetbrains.kotlin.android" version "1.7.10" apply false +} + +include ":app" diff --git a/frontend/asset/fonts/NotoSansKR-Black.otf b/frontend/asset/fonts/NotoSansKR-Black.otf new file mode 100644 index 0000000000..5599581dad Binary files /dev/null and b/frontend/asset/fonts/NotoSansKR-Black.otf differ diff --git a/frontend/asset/fonts/NotoSansKR-Bold.otf b/frontend/asset/fonts/NotoSansKR-Bold.otf new file mode 100644 index 0000000000..be388bf5f9 Binary files /dev/null and b/frontend/asset/fonts/NotoSansKR-Bold.otf differ diff --git a/frontend/asset/fonts/NotoSansKR-Light.otf b/frontend/asset/fonts/NotoSansKR-Light.otf new file mode 100644 index 0000000000..548e667e9b Binary files /dev/null and b/frontend/asset/fonts/NotoSansKR-Light.otf differ diff --git a/frontend/asset/fonts/NotoSansKR-Medium.otf b/frontend/asset/fonts/NotoSansKR-Medium.otf new file mode 100644 index 0000000000..5ddbbc0380 Binary files /dev/null and b/frontend/asset/fonts/NotoSansKR-Medium.otf differ diff --git a/frontend/asset/fonts/NotoSansKR-Regular.otf b/frontend/asset/fonts/NotoSansKR-Regular.otf new file mode 100644 index 0000000000..7c5c2fae3f Binary files /dev/null and b/frontend/asset/fonts/NotoSansKR-Regular.otf differ diff --git a/frontend/asset/fonts/NotoSansKR-Thin.otf b/frontend/asset/fonts/NotoSansKR-Thin.otf new file mode 100644 index 0000000000..1299fef0f3 Binary files /dev/null and b/frontend/asset/fonts/NotoSansKR-Thin.otf differ diff --git a/frontend/asset/imgs/decle.png b/frontend/asset/imgs/decle.png new file mode 100644 index 0000000000..cd48db53eb Binary files /dev/null and b/frontend/asset/imgs/decle.png differ diff --git a/frontend/asset/imgs/logo.png b/frontend/asset/imgs/logo.png new file mode 100644 index 0000000000..38c03f5fe5 Binary files /dev/null and b/frontend/asset/imgs/logo.png differ diff --git a/frontend/asset/imgs/logo_white.png b/frontend/asset/imgs/logo_white.png new file mode 100644 index 0000000000..72b9393ad2 Binary files /dev/null and b/frontend/asset/imgs/logo_white.png differ diff --git a/frontend/asset/jsons/departments.json b/frontend/asset/jsons/departments.json new file mode 100644 index 0000000000..6c0a47d676 --- /dev/null +++ b/frontend/asset/jsons/departments.json @@ -0,0 +1,177 @@ +{ + "인문계열": [ + "언어정보학과", + "한국어문학과", + "독일어문학과", + "노어노문학과", + "영어영문학과", + "일어일문학과", + "중어중문학과", + "불어불문학과", + "서어서문학과", + "북한학과", + "철학과", + "사학과", + "문화인류학과", + "문예창작학과", + "문헌정보학과", + "관광학과", + "한문학과", + "신학과", + "불교학과", + "자율전공학과" + ], + "사회과학계열": [ + "경영학과", + "경제학과", + "경영정보학과", + "국제통상학과", + "광고홍보학과", + "금융학과", + "회계학과", + "세무학과", + "심리학과", + "법학과", + "사회학과", + "도시학과", + "정치외교학과", + "국제학과", + "사회복지학과", + "미디어커뮤니케이션학과", + "지리학과", + "행정학과", + "군사학과", + "경찰행정학과", + "아동가족학과", + "소비자학과", + "물류학과", + "무역학과", + "호텔경영학과" + ], + "교육계열": [ + "가정교육과", + "건설공학교육과", + "과학교육과", + "전기전자통신공학교육과", + "기계재료공학교육과", + "기술교육과", + "농업교육과", + "물리교육과", + "미술교육과", + "사회교육과", + "생물교육과", + "수학교육과", + "수해양산업교육과", + "아동교육과", + "언어치료학과", + "언어교육학과", + "역사교육과", + "음악교육과", + "윤리교육과", + "종교교육과", + "지구과학교육과", + "지리교육과", + "체육교육과", + "초등교육과", + "컴퓨터교육과", + "특수교육과", + "한자교육과", + "화학교육과", + "환경교육과" + ], + "공학계열": [ + "컴퓨터공학과", + "조선공학과", + "산업공학과", + "멀티미디어학과", + "게임공학과", + "재료공학과", + "건축학과", + "물류시스템공학과", + "해양공학과", + "환경공학과", + "고분자공학과", + "광학공학과", + "교통공학과", + "국방기술학과", + "금속공학과", + "금형설계학과", + "기계공학과", + "나노공학과", + "도시공학과", + "로봇공학과", + "무인항공학과", + "반도체학과", + "섬유학과", + "세라믹공학과", + "소방방재학과", + "신소재공학과", + "신재생에너지공학과", + "안전공학과", + "원자력공학과", + "자동차공학과", + "전기전자공학과", + "철도교통학과", + "토목공학과", + "항공우주공학과", + "화학공학과" + ], + "자연과학계열": [ + "생명과학과", + "원예학과", + "조경학과", + "동물학과", + "약학공학과", + "식품과학과", + "수의학과", + "천문학과", + "물리학과", + "수학과", + "화학과", + "의류학과", + "산림공학과", + "지질학과", + "지리학과" + ], + "의학보건계열": [ + "의예과", + "간호학과", + "약학과", + "치의학과", + "물리치료학과", + "한의학과", + "환경보건학과", + "긴급구조학과", + "보건행정학과", + "임상병리학과", + "방사선학과", + "미술치료학과", + "언어재활학과" + ], + "예체능계열": [ + "응용미술학과", + "조소과", + "회화과", + "연극영화학과", + "그래픽디자인학과", + "영상디자인학과", + "산업디자인학과", + "공업디자인학과", + "공간디자인학과", + "시각디자인학과", + "금속공예학과", + "애니메이션학과", + "실용음악학과", + "성악학과", + "응용음악학과", + "패션디자인학과", + "실내디자인학과", + "광고디자인학과", + "무용스포츠학과", + "사회체육학과", + "건강관리학과", + "메이크업아티스트학과", + "모델학과", + "조리제빵학과" + ] +} \ No newline at end of file diff --git a/frontend/ios/.gitignore b/frontend/ios/.gitignore new file mode 100644 index 0000000000..7a7f9873ad --- /dev/null +++ b/frontend/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/frontend/ios/Flutter/AppFrameworkInfo.plist b/frontend/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000000..7c56964006 --- /dev/null +++ b/frontend/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/frontend/ios/Flutter/Debug.xcconfig b/frontend/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000000..ec97fc6f30 --- /dev/null +++ b/frontend/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/frontend/ios/Flutter/Release.xcconfig b/frontend/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000000..c4855bfe20 --- /dev/null +++ b/frontend/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/frontend/ios/Podfile b/frontend/ios/Podfile new file mode 100644 index 0000000000..155a4eabb3 --- /dev/null +++ b/frontend/ios/Podfile @@ -0,0 +1,92 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ + '$(inherited)', + + ## dart: PermissionGroup.calendar + # 'PERMISSION_EVENTS=1', + + ## dart: PermissionGroup.reminders + # 'PERMISSION_REMINDERS=1', + + ## dart: PermissionGroup.contacts + # 'PERMISSION_CONTACTS=1', + + ## dart: PermissionGroup.camera + # 'PERMISSION_CAMERA=1', + + ## dart: PermissionGroup.microphone + # 'PERMISSION_MICROPHONE=1', + + ## dart: PermissionGroup.speech + # 'PERMISSION_SPEECH_RECOGNIZER=1', + + # dart: PermissionGroup.photos + 'PERMISSION_PHOTOS=1', + + ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse] + # 'PERMISSION_LOCATION=1', + + ## dart: PermissionGroup.notification + 'PERMISSION_NOTIFICATIONS=1', + + ## dart: PermissionGroup.mediaLibrary + # 'PERMISSION_MEDIA_LIBRARY=1', + + ## dart: PermissionGroup.sensors + # 'PERMISSION_SENSORS=1', + + ## dart: PermissionGroup.bluetooth + # 'PERMISSION_BLUETOOTH=1', + + ## dart: PermissionGroup.appTrackingTransparency + # 'PERMISSION_APP_TRACKING_TRANSPARENCY=1', + + ## dart: PermissionGroup.criticalAlerts + # 'PERMISSION_CRITICAL_ALERTS=1' + ] + end + end +end \ No newline at end of file diff --git a/frontend/ios/Podfile.lock b/frontend/ios/Podfile.lock new file mode 100644 index 0000000000..b839c4da96 --- /dev/null +++ b/frontend/ios/Podfile.lock @@ -0,0 +1,71 @@ +PODS: + - Flutter (1.0.0) + - flutter_email_sender (0.0.1): + - Flutter + - flutter_keyboard_visibility (0.0.1): + - Flutter + - flutter_local_notifications (0.0.1): + - Flutter + - flutter_secure_storage (6.0.0): + - Flutter + - image_picker_ios (0.0.1): + - Flutter + - package_info_plus (0.4.5): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - permission_handler_apple (9.3.0): + - Flutter + - url_launcher_ios (0.0.1): + - Flutter + +DEPENDENCIES: + - Flutter (from `Flutter`) + - flutter_email_sender (from `.symlinks/plugins/flutter_email_sender/ios`) + - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) + - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) + - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + flutter_email_sender: + :path: ".symlinks/plugins/flutter_email_sender/ios" + flutter_keyboard_visibility: + :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" + flutter_local_notifications: + :path: ".symlinks/plugins/flutter_local_notifications/ios" + flutter_secure_storage: + :path: ".symlinks/plugins/flutter_secure_storage/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + +SPEC CHECKSUMS: + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_email_sender: 10a22605f92809a11ef52b2f412db806c6082d40 + flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 + flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 + flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 + image_picker_ios: b545a5f16c0fa88e3ecbbce3ed4de45567a8ec18 + package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + +PODFILE CHECKSUM: 217523224daf8c931a48b4f7c582a7b6c6949104 + +COCOAPODS: 1.15.2 diff --git a/frontend/ios/Runner.xcodeproj/project.pbxproj b/frontend/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..1b2236acc6 --- /dev/null +++ b/frontend/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,747 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 49CBC05723C47CDB5F644AB5 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DA2E123D24BEF76DA9BAB1A /* Pods_Runner.framework */; }; + 4E958C3AFD9C0951935C5054 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6AF14DD9A2C4AA83D9347CFF /* Pods_RunnerTests.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 138A0B0A223E5456C4E18D17 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 241FF8CF0BBC0801D1132AA8 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 5DA2E123D24BEF76DA9BAB1A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6AF14DD9A2C4AA83D9347CFF /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 73CB1078547AEB8878C83934 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + EACB1D4EA3E3850188231F03 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + EF46E0AF6AC00A8B475D3FC1 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + F627AB332BE9A95500EBE3EB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + F742FB545166D3B75107A530 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 49CBC05723C47CDB5F644AB5 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AE4F8416C6BFB49D8EE2E78D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4E958C3AFD9C0951935C5054 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 07F16D8A05914CD430D4512B /* Pods */ = { + isa = PBXGroup; + children = ( + F742FB545166D3B75107A530 /* Pods-Runner.debug.xcconfig */, + EACB1D4EA3E3850188231F03 /* Pods-Runner.release.xcconfig */, + 241FF8CF0BBC0801D1132AA8 /* Pods-Runner.profile.xcconfig */, + 138A0B0A223E5456C4E18D17 /* Pods-RunnerTests.debug.xcconfig */, + EF46E0AF6AC00A8B475D3FC1 /* Pods-RunnerTests.release.xcconfig */, + 73CB1078547AEB8878C83934 /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 904513FECE7BF94BCC1F158F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5DA2E123D24BEF76DA9BAB1A /* Pods_Runner.framework */, + 6AF14DD9A2C4AA83D9347CFF /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 07F16D8A05914CD430D4512B /* Pods */, + 904513FECE7BF94BCC1F158F /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + F627AB332BE9A95500EBE3EB /* Runner.entitlements */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + E2307C21059A208728E8F61D /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + AE4F8416C6BFB49D8EE2E78D /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 20B493236EA37E35F32E185E /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ADF198237E3E5683F0FAC9AD /* [CP] Embed Pods Frameworks */, + BC9DF3D2FB9D4E7B73E10365 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 20B493236EA37E35F32E185E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + ADF198237E3E5683F0FAC9AD /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + BC9DF3D2FB9D4E7B73E10365 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + E2307C21059A208728E8F61D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 958A684W35; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.capstone.frontend; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 138A0B0A223E5456C4E18D17 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.capstone.frontend.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EF46E0AF6AC00A8B475D3FC1 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.capstone.frontend.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 73CB1078547AEB8878C83934 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.capstone.frontend.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 958A684W35; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.capstone.frontend; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 958A684W35; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.capstone.frontend; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/frontend/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/frontend/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/frontend/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000000..f9b0d7c5ea --- /dev/null +++ b/frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/frontend/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000000..87131a09be --- /dev/null +++ b/frontend/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/ios/Runner.xcworkspace/contents.xcworkspacedata b/frontend/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..21a3cc14c7 --- /dev/null +++ b/frontend/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/frontend/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/frontend/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/frontend/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/frontend/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000000..f9b0d7c5ea --- /dev/null +++ b/frontend/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/frontend/ios/Runner/AppDelegate.swift b/frontend/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000000..44f2c4f0d1 --- /dev/null +++ b/frontend/ios/Runner/AppDelegate.swift @@ -0,0 +1,17 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate + } + + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png new file mode 100644 index 0000000000..5a36c0ac26 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/102.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/102.png new file mode 100644 index 0000000000..0c725fbb75 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/102.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000000..7e5f9d18c4 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 0000000000..ff8d9157d6 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 0000000000..b1c7374df8 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png new file mode 100644 index 0000000000..ef865bfb27 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png new file mode 100644 index 0000000000..d483546fcf Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png new file mode 100644 index 0000000000..4b4d61861d Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png new file mode 100644 index 0000000000..25423d29bc Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 0000000000..f9744a9448 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png new file mode 100644 index 0000000000..09d2413365 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 0000000000..9f8c3b0bea Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png new file mode 100644 index 0000000000..ee9900a3c5 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 0000000000..30bbf2e8a8 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png new file mode 100644 index 0000000000..c059fca565 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png new file mode 100644 index 0000000000..2495726df2 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 0000000000..109e701587 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png new file mode 100644 index 0000000000..d2b76e1765 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 0000000000..ef37001575 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png new file mode 100644 index 0000000000..cc0bdcc03a Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png new file mode 100644 index 0000000000..12c4b8a48d Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png new file mode 100644 index 0000000000..f5edfa254a Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png new file mode 100644 index 0000000000..dfcd646af0 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 0000000000..a46327b5e7 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 0000000000..e7756a917e Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 0000000000..946d6032f8 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png new file mode 100644 index 0000000000..3678d60d40 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/66.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/66.png new file mode 100644 index 0000000000..90374571aa Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/66.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png new file mode 100644 index 0000000000..8fba0cccd4 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 0000000000..207f1507a6 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 0000000000..5ca87b47fe Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 0000000000..37ca16e642 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png new file mode 100644 index 0000000000..5e01205b81 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/92.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/92.png new file mode 100644 index 0000000000..5717bd6314 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/92.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..ffab2548e6 --- /dev/null +++ b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"idiom":"watch","filename":"172.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"86x86","expected-size":"172","role":"quickLook"},{"idiom":"watch","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"40x40","expected-size":"80","role":"appLauncher"},{"idiom":"watch","filename":"88.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"40mm","scale":"2x","size":"44x44","expected-size":"88","role":"appLauncher"},{"idiom":"watch","filename":"102.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"41mm","scale":"2x","size":"45x45","expected-size":"102","role":"appLauncher"},{"idiom":"watch","filename":"92.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"41mm","scale":"2x","size":"46x46","expected-size":"92","role":"appLauncher"},{"idiom":"watch","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"50x50","expected-size":"100","role":"appLauncher"},{"idiom":"watch","filename":"196.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"98x98","expected-size":"196","role":"quickLook"},{"idiom":"watch","filename":"216.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"108x108","expected-size":"216","role":"quickLook"},{"idiom":"watch","filename":"48.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"24x24","expected-size":"48","role":"notificationCenter"},{"idiom":"watch","filename":"55.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"27.5x27.5","expected-size":"55","role":"notificationCenter"},{"idiom":"watch","filename":"66.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"45mm","scale":"2x","size":"33x33","expected-size":"66","role":"notificationCenter"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"3x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"2x"},{"size":"1024x1024","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch-marketing","scale":"1x"},{"size":"128x128","expected-size":"128","filename":"128.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]} \ No newline at end of file diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000..dc9ada4725 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000000..7353c41ecf Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000..797d452e45 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000..6ed2d933e1 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000..4cd7b0099c Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000..fe730945a0 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000000..321773cd85 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000000..797d452e45 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000..502f463a9b Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000000..0ec3034392 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000..0ec3034392 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000..e9f5fea27c Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000000..84ac32ae7d Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000000..8953cba090 Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000..0467bf12aa Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000000..0bedcf2fd4 --- /dev/null +++ b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000..9da19eacad Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000..9da19eacad Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000..9da19eacad Binary files /dev/null and b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000000..89c2725b70 --- /dev/null +++ b/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/frontend/ios/Runner/Base.lproj/LaunchScreen.storyboard b/frontend/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..f2e259c7c9 --- /dev/null +++ b/frontend/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/ios/Runner/Base.lproj/Main.storyboard b/frontend/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000000..f3c28516fb --- /dev/null +++ b/frontend/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/ios/Runner/Info.plist b/frontend/ios/Runner/Info.plist new file mode 100644 index 0000000000..13fcb21849 --- /dev/null +++ b/frontend/ios/Runner/Info.plist @@ -0,0 +1,55 @@ + + + + + NSCameraUsageDescription + Used to demonstrate image picker plugin + NSMicrophoneUsageDescription + Used to capture audio for image picker plugin + NSPhotoLibraryUsageDescription + 게시글 사진 업로드를 위해 사진 접근 허용을 해주세요. 설정에서 이를 변경할 수 있습니다. + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + 디클 + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + frontend + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/frontend/ios/Runner/Runner-Bridging-Header.h b/frontend/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000000..308a2a560b --- /dev/null +++ b/frontend/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/frontend/ios/Runner/Runner.entitlements b/frontend/ios/Runner/Runner.entitlements new file mode 100644 index 0000000000..903def2af5 --- /dev/null +++ b/frontend/ios/Runner/Runner.entitlements @@ -0,0 +1,8 @@ + + + + + aps-environment + development + + diff --git a/frontend/ios/RunnerTests/RunnerTests.swift b/frontend/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000000..86a7c3b1b6 --- /dev/null +++ b/frontend/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/frontend/lib/board/component/board_card.dart b/frontend/lib/board/component/board_card.dart new file mode 100644 index 0000000000..8d3c1633d2 --- /dev/null +++ b/frontend/lib/board/component/board_card.dart @@ -0,0 +1,213 @@ +import 'package:flutter/material.dart'; + +import '../../common/const/colors.dart'; +import '../const/categorys.dart'; +import '../model/msg_board_response_model.dart'; +import 'text_with_icon_for_view.dart'; + +class BoardCard extends StatelessWidget { + final int id; + final int userId; + final String userNickname; + final String universityName; + final int communityId; + final String communityTitle; + final String postTitle; + final String postContent; + final List images; + final ReactCountModel count; + final bool isQuestion; + final int imageCount; + final String createdDateTime; + + const BoardCard({ + required this.id, + required this.userId, + required this.userNickname, + required this.universityName, + required this.communityId, + required this.communityTitle, + required this.postTitle, + required this.postContent, + required this.images, + required this.count, + required this.isQuestion, + required this.imageCount, + required this.createdDateTime, + super.key, + }); + + factory BoardCard.fromModel( + {required MsgBoardResponseModel msgBoardResponseModel}) { + return BoardCard( + id: msgBoardResponseModel.id, + userId: msgBoardResponseModel.userId, + userNickname: msgBoardResponseModel.userNickname, + universityName: msgBoardResponseModel.universityName, + communityId: msgBoardResponseModel.communityId, + communityTitle: msgBoardResponseModel.communityTitle, + postTitle: msgBoardResponseModel.postTitle, + postContent: msgBoardResponseModel.postContent, + images: msgBoardResponseModel.images, + count: msgBoardResponseModel.count, + isQuestion: msgBoardResponseModel.isQuestion, + imageCount: msgBoardResponseModel.imageCount, + createdDateTime: msgBoardResponseModel.createdDateTime, + ); + } + + String changeTime(String time) { + DateTime t = DateTime.parse(time); + + time = time.replaceAll('T', " "); + + if (DateTime.now().difference(t).inDays == 0) { + int diffM = DateTime.now().difference(t).inMinutes; + if (diffM < 60) { + if (diffM == 0) { + return "방금전"; + } + return "$diffM분전"; + } + } + + return time.replaceRange(16, time.length, ""); + } + + String _formatText(String text, int maxLength) { + // 줄바꿈을 기준으로 첫 줄만 추출 + String firstLine = text.split('\n')[0]; + if (firstLine.length > maxLength) { + return '${firstLine.substring(0, maxLength)}...'; + } + return firstLine; + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: BODY_TEXT_COLOR.withOpacity(0.5), + width: 1, + ), + ), + ), + margin: const EdgeInsets.only( + top: 10, + left: 10, + right: 10, + ), + child: Padding( + padding: const EdgeInsets.only( + bottom: 10, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _renderCategoryCircleUi( + categoryCodesReverseList[communityTitle] ?? communityTitle), + Row( + children: [ + TextWithIconForView( + icon: Icons.favorite_outline_rounded, + iconSize: 15, + text: count.likeCount.toString(), + color: Colors.red, + ), + const SizedBox( + width: 13, + ), + TextWithIconForView( + icon: Icons.chat_outlined, + iconSize: 15, + text: count.commentReplyCount.toString(), + ), + const SizedBox( + width: 13, + ), + TextWithIconForView( + icon: Icons.star_outline_rounded, + iconSize: 18, + text: count.scrapCount.toString(), + color: Colors.orangeAccent, + ), + const SizedBox( + width: 13, + ), + TextWithIconForView( + icon: Icons.photo_size_select_actual_outlined, + iconSize: 18, + text: imageCount.toString(), + color: Colors.black, + ), + const SizedBox( + width: 13, + ), + ], + ), + ], + ), + const SizedBox( + height: 5, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _formatText(postTitle, 40), + style: const TextStyle( + fontSize: 12.0, + fontWeight: FontWeight.bold, + ), + ), + Text( + _formatText(postContent, 80), + softWrap: true, + style: const TextStyle( + fontSize: 10, + ), + ), + const SizedBox( + height: 5, + ), + Text( + "${changeTime(createdDateTime)} | $userNickname", + style: const TextStyle(fontSize: 10), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _renderCategoryCircleUi(String category) { + return Container( + // category circle + decoration: BoxDecoration( + color: PRIMARY10_COLOR, + borderRadius: BorderRadius.circular(50), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 3), + child: Center( + child: Text( + category, + style: const TextStyle( + fontSize: 10, + ), + ), + ), + ), + ); + } +} diff --git a/frontend/lib/board/component/category_circle_with_provider.dart b/frontend/lib/board/component/category_circle_with_provider.dart new file mode 100644 index 0000000000..f31393c027 --- /dev/null +++ b/frontend/lib/board/component/category_circle_with_provider.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../common/const/colors.dart'; +import '../provider/api_category_provider.dart'; +import '../provider/category_provider.dart'; + +class CategoryCircleWithProvider extends ConsumerWidget { + const CategoryCircleWithProvider({ + super.key, + required this.category, + required this.categoryCode, + required this.type, + }); + + final String category; + final String? categoryCode; + final bool type; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final clickedList = ref.watch(categoryStateProvider); + + return GestureDetector( + onTap: () { + if (categoryCode == "HOT") { + ref.read(categoryTitleProvider.notifier).state = null; + ref.read(isHotProvider.notifier).state = true; + } else if (categoryCode == "ALL") { + ref.read(categoryTitleProvider.notifier).state = null; + ref.read(isHotProvider.notifier).state = false; + } else { + ref.read(categoryTitleProvider.notifier).state = categoryCode; + ref.read(isHotProvider.notifier).state = false; + } + + ref.read(categoryStateProvider.notifier).clear(); + ref.read(categoryStateProvider.notifier).add(category); + }, + child: Container( + // category circle + decoration: BoxDecoration( + color: !type + ? PRIMARY10_COLOR + : !clickedList.contains(category) + ? BODY_TEXT_COLOR.withOpacity(0.1) + : Colors.white, + borderRadius: BorderRadius.circular(50), + border: type && clickedList.contains(category) + ? Border.all( + color: PRIMARY_COLOR, + ) + : null, + ), + child: SizedBox( + height: 40.0, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 3), + child: Center( + child: Text( + category, + style: const TextStyle( + fontSize: 10, + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/frontend/lib/board/component/text_with_icon_for_view.dart b/frontend/lib/board/component/text_with_icon_for_view.dart new file mode 100644 index 0000000000..2a2c2aaf99 --- /dev/null +++ b/frontend/lib/board/component/text_with_icon_for_view.dart @@ -0,0 +1,44 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class TextWithIconForView extends StatefulWidget { + final IconData icon; + final double iconSize; + final String text; + final Color? color; + + const TextWithIconForView({ + super.key, + required this.icon, + required this.iconSize, + required this.text, + this.color, + }); + + @override + State createState() => _TextWithIconForViewState(); +} + +class _TextWithIconForViewState extends State { + @override + Widget build(BuildContext context) { + return Row( + children: [ + Icon( + widget.icon, + color: widget.color, + size: widget.iconSize, + ), + const SizedBox( + width: 2, + ), + Text( + widget.text, + style: const TextStyle( + fontSize: 12, + ), + ), + ], + ); + } +} diff --git a/frontend/lib/board/const/categorys.dart b/frontend/lib/board/const/categorys.dart new file mode 100644 index 0000000000..0866b01750 --- /dev/null +++ b/frontend/lib/board/const/categorys.dart @@ -0,0 +1,28 @@ +const List categorysList = [ + "전체", + "인기게시판", + "자유게시판", + "대학원게시판", + "취준게시판", + "스터디모집", + "홍보게시판", +]; + +Map categoryCodesList = { + "전체": "ALL", + "인기게시판": "HOT", + "자유게시판": "FREE", + "대학원게시판": "GRADUATE", + "취준게시판": "JOB", + "스터디모집": "STUDY", + "홍보게시판": "PROMOTION", +}; + +Map categoryCodesReverseList = { + "HOT": "인기게시판", + "FREE": "자유게시판", + "GRADUATE": "대학원게시판", + "JOB": "취준게시판", + "STUDY": "스터디모집", + "PROMOTION": "홍보게시판", +}; diff --git a/frontend/lib/board/const/report_reason.dart b/frontend/lib/board/const/report_reason.dart new file mode 100644 index 0000000000..bcf4b15a7a --- /dev/null +++ b/frontend/lib/board/const/report_reason.dart @@ -0,0 +1,8 @@ +Map reportReasonList = { + "욕설/비하": "INSULTING", + "상업적 광고 및 판매": "COMMERCIAL", + "게시판 성격에 부적절함": "INAPPROPRIATE", + "유출/사칭/사기": "FRAUD", + "낚시/놀람/도배": "SPAM", + "음란물/불건전한 만남 및 대화": "PORNOGRAPHIC", +}; diff --git a/frontend/lib/board/layout/board_layout.dart b/frontend/lib/board/layout/board_layout.dart new file mode 100644 index 0000000000..8b3acde228 --- /dev/null +++ b/frontend/lib/board/layout/board_layout.dart @@ -0,0 +1,316 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/board/const/categorys.dart'; +import 'package:frontend/board/model/msg_board_detail_response_model.dart'; +import 'package:frontend/board/model/msg_board_response_model.dart'; +import 'package:frontend/common/const/colors.dart'; +import 'package:frontend/board/layout/category_circle_layout.dart'; +import 'package:frontend/board/layout/text_with_icon.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:smooth_page_indicator/smooth_page_indicator.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class Board extends ConsumerWidget { + final MsgBoardDetailResponseModel board; + final bool isMine; + final double titleSize; + const Board({ + super.key, + required this.board, + required this.titleSize, + required this.isMine, + }); + + String changeTime(String time) { + DateTime t = DateTime.parse(time); + + time = time.replaceAll('T', " "); + + if (DateTime.now().difference(t).inDays == 0) { + int diffM = DateTime.now().difference(t).inMinutes; + if (diffM < 60) { + if (diffM == 0) { + return "방금전"; + } + return "$diffM분전"; + } + } + + return time.replaceRange(16, time.length, ""); + } + + void _launchUrl(String url) async { + if (await canLaunchUrlString(url)) { + await launchUrlString(url); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + MsgBoardResponseModel boardForImageViewer = board; + + String allText = board.postContent; + List splitText = allText.split("\n"); + + List spans = []; + for (String t in splitText) { + if (t.startsWith('http')) { + spans.add( + TextSpan( + text: '$t\n', + style: const TextStyle( + color: Colors.blue, + decoration: TextDecoration.underline, + fontSize: 12, + ), + recognizer: TapGestureRecognizer()..onTap = () => _launchUrl(t), + ), + ); + } else { + spans.add( + TextSpan( + text: '$t\n', + style: const TextStyle( + color: Colors.black, + fontSize: 12, + ), + ), + ); + } + } + + return Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: BODY_TEXT_COLOR.withOpacity(0.5), + width: 1, + ), + ), + ), + margin: const EdgeInsets.only( + top: 10, + left: 10, + right: 10, + ), + child: Padding( + padding: const EdgeInsets.only( + bottom: 10, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CategoryCircle( + category: + categoryCodesReverseList[board.communityTitle].toString(), + type: false, + ), + Row( + children: [ + TextWithIcon( + icon: Icons.favorite_outline_rounded, + iconSize: 15, + text: board.count.likeCount.toString(), + commentId: -1, + postId: board.id, + replyId: -1, + isClicked: board.likedBy, + isMine: isMine, + userId: board.userId, + ), + TextWithIcon( + icon: Icons.chat_outlined, + iconSize: 15, + text: board.count.commentReplyCount.toString(), + commentId: -1, + postId: -1, + replyId: -1, + isClicked: false, + isMine: isMine, + userId: board.userId, + ), + TextWithIcon( + icon: Icons.star_outline_rounded, + iconSize: 18, + text: board.count.scrapCount.toString(), + commentId: -1, + postId: board.id, + replyId: -1, + isClicked: board.isScrapped, + isMine: isMine, + userId: board.userId, + ), + const SizedBox( + width: 6, + ), + ], + ), + ], + ), + const SizedBox( + height: 5, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + board.postTitle, + style: TextStyle( + fontSize: titleSize, + fontWeight: FontWeight.bold, + ), + ), + // Text( + // board.postContent, + // softWrap: true, + // style: const TextStyle( + // fontSize: 10, + // ), + // ), + const SizedBox( + height: 5, + ), + RichText( + softWrap: true, + text: TextSpan( + children: spans, + ), + ), + const SizedBox( + height: 5, + ), + ImageViewer( + board: boardForImageViewer, + ), + Text( + "${changeTime(board.createdDateTime)} | ${board.userNickname}", + style: const TextStyle(fontSize: 10), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class ImageViewer extends StatelessWidget { + final MsgBoardResponseModel board; + const ImageViewer({super.key, required this.board}); + + @override + Widget build(BuildContext context) { + if (board.images.isEmpty) { + return const SizedBox( + height: 5, + ); + } + return Padding( + padding: const EdgeInsets.only(bottom: 10, top: 10), + child: SizedBox( + height: 100, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + for (var imageLink in board.images) + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ShowImageBigger( + imageLink: board.images, + index: board.images.indexOf(imageLink), + ), + fullscreenDialog: true), + ); + }, + child: Container( + margin: const EdgeInsets.only( + right: 10, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.black.withOpacity(0.2), + ), + width: 100, + child: Image( + image: NetworkImage(imageLink), + ), + ), + ), + ], + ), + ), + ); + } +} + +class ShowImageBigger extends StatelessWidget { + ShowImageBigger({super.key, required this.imageLink, required this.index}); + final List imageLink; + final int index; + final PageController _controller = PageController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.black, + leading: IconButton( + icon: const Icon( + Icons.close, + color: Colors.white, + ), + onPressed: () { + Navigator.of(context).pop(); + }, + )), + body: Stack( + children: [ + PageView( + controller: _controller, + children: [ + for (var i = 0; i < imageLink.length; i++) + Container( + color: Colors.black, + padding: const EdgeInsets.only(bottom: 100), + child: Center( + child: PhotoView( + imageProvider: NetworkImage(imageLink[i]), + minScale: PhotoViewComputedScale.contained, + maxScale: PhotoViewComputedScale.covered * 2, + ), + ), + ), + ], + ), + Container( + margin: const EdgeInsets.only(top: 500), + child: Center( + child: SmoothPageIndicator( + effect: const WormEffect( + dotHeight: 10, + dotWidth: 10, + type: WormType.thinUnderground, + dotColor: Colors.white, + activeDotColor: PRIMARY_COLOR, + ), + controller: _controller, + count: imageLink.length, + ), + ), + ), + ], + ), + ); + } +} diff --git a/frontend/lib/board/layout/category_circle_layout.dart b/frontend/lib/board/layout/category_circle_layout.dart new file mode 100644 index 0000000000..ea24b697eb --- /dev/null +++ b/frontend/lib/board/layout/category_circle_layout.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/common/const/colors.dart'; +import 'package:frontend/board/provider/category_provider.dart'; + +class CategoryCircle extends ConsumerWidget { + const CategoryCircle({ + super.key, + required this.category, + required this.type, + }); + + final String category; + final bool type; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final clickedList = ref.watch(categoryStateProvider); + return GestureDetector( + onTap: () { + if (clickedList.contains(category)) { + ref.read(categoryStateProvider.notifier).remove(category); + } else { + ref.read(categoryStateProvider.notifier).clear(); + ref.read(categoryStateProvider.notifier).add(category); + } + }, + child: Container( + // category circle + decoration: BoxDecoration( + color: !type + ? PRIMARY10_COLOR + : !clickedList.contains(category) + ? BODY_TEXT_COLOR.withOpacity(0.1) + : Colors.white, + borderRadius: BorderRadius.circular(50), + border: type && clickedList.contains(category) + ? Border.all( + color: PRIMARY_COLOR, + ) + : null, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 3), + child: Text( + category, + style: const TextStyle( + fontSize: 10, + ), + ), + ), + ), + ); + } +} diff --git a/frontend/lib/board/layout/comment_layout.dart b/frontend/lib/board/layout/comment_layout.dart new file mode 100644 index 0000000000..a7bc091f95 --- /dev/null +++ b/frontend/lib/board/layout/comment_layout.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/common/const/colors.dart'; +import 'package:frontend/board/model/comment_model.dart'; +import 'package:frontend/board/layout/reply_layout.dart'; +import 'package:frontend/board/layout/text_with_icon.dart'; + +class Comment extends ConsumerStatefulWidget { + final CommentModel comment; + final bool selectComment; + final int selectReplyIndex; + final bool isMine; + final int myId; + const Comment({ + super.key, + required this.comment, + required this.selectComment, + required this.selectReplyIndex, + required this.isMine, + required this.myId, + }); + + @override + ConsumerState createState() => _CommentState(); +} + +class _CommentState extends ConsumerState + with SingleTickerProviderStateMixin { + late AnimationController animationController; + List replies = []; + + @override + void initState() { + super.initState(); + animationController = + AnimationController(vsync: this, duration: const Duration(seconds: 1)); + replies = widget.comment.replies; + for (int i = replies.length - 1; i >= 0; i--) { + if (replies[i].isBlockedUser) { + replies.removeAt(i); + } + } + } + + String changeTime(String time) { + DateTime t = DateTime.parse(time); + + time = time.replaceAll('T', " "); + + if (DateTime.now().difference(t).inDays == 0) { + int diffM = DateTime.now().difference(t).inMinutes; + if (diffM < 60) { + if (diffM == 0) { + return "방금전"; + } + return "$diffM분전"; + } + } + + return time.replaceRange(16, time.length, ""); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: BODY_TEXT_COLOR.withOpacity(0.5), + width: 1, + ), + ), + ), + margin: const EdgeInsets.symmetric(horizontal: 10), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 10, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + widget.comment.userInformation.nickname, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: widget.selectComment ? PRIMARY_COLOR : null, + ), + ), + const SizedBox( + width: 10, + ), + Text( + changeTime(widget.comment.createdAt), + style: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50), + color: BODY_TEXT_COLOR.withOpacity(0.1), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 3), + child: Row( + children: [ + TextWithIcon( + icon: Icons.favorite_outline_rounded, + iconSize: 15, + text: widget.comment.likeCount.count.toString(), + commentId: + widget.comment.deleted ? -3 : widget.comment.id, + postId: -1, + replyId: -1, + isClicked: widget.comment.isLiked, + isMine: widget.isMine, + userId: widget.comment.userId, + ), + TextWithIcon( + icon: Icons.chat_outlined, + iconSize: 15, + text: widget.comment.replies.length.toString(), + commentId: + widget.comment.deleted ? -2 : widget.comment.id, + postId: -1, + replyId: -1, + isClicked: false, + isMine: widget.isMine, + userId: widget.comment.userId, + ), + TextWithIcon( + icon: Icons.more_horiz, + iconSize: 20, + text: "-1", + commentId: + widget.comment.deleted ? -3 : widget.comment.id, + postId: -1, + replyId: -1, + isClicked: false, + isMine: widget.isMine, + userId: widget.comment.userId, + ), + ], + ), + ), + ), + ], + ), + Text( + widget.comment.content, + style: TextStyle( + fontSize: 12, + color: widget.selectComment ? PRIMARY_COLOR : null, + ), + ), + for (var reply in replies) + Reply( + reply: reply, + selectReply: widget.selectReplyIndex == reply.id, + isMine: widget.myId == reply.userId, + myId: widget.myId, + ) + ], + ), + ), + ); + } +} diff --git a/frontend/lib/board/layout/reply_layout.dart b/frontend/lib/board/layout/reply_layout.dart new file mode 100644 index 0000000000..9e5574e305 --- /dev/null +++ b/frontend/lib/board/layout/reply_layout.dart @@ -0,0 +1,172 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/board/layout/text_with_icon.dart'; +import 'package:frontend/board/model/comment_model.dart'; +import 'package:frontend/common/const/colors.dart'; + +class Reply extends ConsumerStatefulWidget { + final ReplyModel reply; + final bool selectReply; + final bool isMine; + final int myId; + const Reply({ + super.key, + required this.reply, + required this.selectReply, + required this.isMine, + required this.myId, + }); + + @override + ConsumerState createState() => _Reply(); +} + +class _Reply extends ConsumerState { + @override + void initState() { + super.initState(); + } + + String changeTime(String time) { + DateTime t = DateTime.parse(time); + + time = time.replaceAll('T', " "); + + if (DateTime.now().difference(t).inDays == 0) { + int diffM = DateTime.now().difference(t).inMinutes; + if (diffM < 60) { + if (diffM == 0) { + return "방금전"; + } + return "$diffM분전"; + } + } + + return time.replaceRange(16, time.length, ""); + } + + @override + Widget build(BuildContext context) { + bool myClicked = false; + for (var likes in widget.reply.likeCount.likes) { + myClicked = likes.usersId == widget.myId; + } + return Column( + children: [ + const SizedBox( + height: 10, + ), + Row( + children: [ + Transform.flip( + flipY: true, + child: const Icon( + Icons.turn_right_rounded, + ), + ), + const SizedBox( + width: 5, + ), + Expanded( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: BODY_TEXT_COLOR.withOpacity(0.1), + ), + child: Padding( + padding: const EdgeInsets.only( + top: 15, + left: 15, + right: 15, + bottom: 15, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + widget.reply.userInformation.nickname, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: widget.selectReply + ? PRIMARY_COLOR + : Colors.black, + ), + ), + const SizedBox( + width: 10, + ), + Text( + changeTime(widget.reply.createdAt), + style: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50), + color: BODY_TEXT_COLOR.withOpacity(0.1), + ), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 3), + child: Row( + children: [ + TextWithIcon( + icon: Icons.favorite_outline_rounded, + iconSize: 15, + text: + widget.reply.likeCount.count.toString(), + commentId: -1, + postId: -1, + replyId: widget.reply.id, + isClicked: myClicked, + isMine: widget.isMine, + userId: widget.reply.userId, + ), + TextWithIcon( + icon: Icons.more_horiz, + iconSize: 20, + text: "-1", + commentId: -1, + postId: -1, + replyId: widget.reply.id, + isClicked: false, + isMine: widget.isMine, + userId: widget.reply.userId, + ), + ], + ), + ), + ), + ], + ), + Text( + widget.reply.content, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.normal, + color: + widget.selectReply ? PRIMARY_COLOR : Colors.black, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ], + ); + } +} diff --git a/frontend/lib/board/layout/text_with_icon.dart b/frontend/lib/board/layout/text_with_icon.dart new file mode 100644 index 0000000000..46e5b64e94 --- /dev/null +++ b/frontend/lib/board/layout/text_with_icon.dart @@ -0,0 +1,905 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/board/provider/block_provider.dart'; +import 'package:frontend/board/provider/board_add_provider.dart'; +import 'package:frontend/board/provider/cloudwatch_provider.dart'; +import 'package:frontend/board/provider/comment_provider.dart'; +import 'package:frontend/board/provider/isquestion_provider.dart'; +import 'package:frontend/board/provider/image_provider.dart'; +import 'package:frontend/board/provider/comment_notifier_provider.dart'; +import 'package:frontend/board/provider/reply_notifier_provider.dart'; +import 'package:frontend/board/provider/reply_provider.dart'; +import 'package:frontend/board/provider/report_provider.dart'; +import 'package:frontend/board/provider/scrap_provider.dart'; +import 'package:frontend/common/const/colors.dart'; +import 'package:frontend/member/provider/mypage/my_scrap_state_notifier_provider.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class TextWithIcon extends ConsumerStatefulWidget { + final IconData icon; + final double iconSize; + final String text; + final int commentId; + final int postId; + final int replyId; + final bool isClicked; + final bool isMine; + final int userId; + + const TextWithIcon({ + super.key, + required this.icon, + required this.iconSize, + required this.text, + required this.commentId, + required this.postId, + required this.replyId, + required this.isClicked, + required this.isMine, + required this.userId, + }); + + @override + ConsumerState createState() => _TextWithIconState(); +} + +class _TextWithIconState extends ConsumerState + with SingleTickerProviderStateMixin { + bool isHeartClicked = false; + bool isFavoriteClicked = false; + bool isQuestionClicked = false; + bool cantClicked = true; + // ignore: prefer_typing_uninitialized_variables + var textCount; + + late AnimationController heartAnimationController; + final ImagePicker picker = ImagePicker(); + + Future getImage() async { + // PermissionStatus status = await Permission.photos.status; + // if (!status.isGranted) { + // return "게시글 사진 업로드를 위해\n사진 접근 허용을 해주세요"; + // } + List images = []; + try { + images = await picker.pickMultiImage(); + } catch (e) { + debugPrint("getImageError : $e"); + ref.watch(cloudWatchStateProvider.notifier).add(e.toString()); + return "게시글 사진 업로드를 위해 사진 접근 허용을 해주세요. 설정에서 이를 변경할 수 있습니다."; // permission access need! + } + ref.read(imageStateProvider.notifier).add(images); + return ""; + } + + void notAllowed(String s) { + showDialog( + context: context, + builder: ((context) { + return AlertDialog( + content: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + s, + overflow: TextOverflow.visible, + style: const TextStyle( + color: Colors.black, + fontSize: 13, + ), + ), + ), + ], + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: PRIMARY50_COLOR, + ), + child: const Text( + "확인", + style: TextStyle(fontSize: 13, color: PRIMARY_COLOR), + ), + ), + ], + ), + ], + ); + })); + } + + void increaseHeart() { + setState(() { + textCount += 1; + isHeartClicked = true; + }); + heartAnimationController.forward(); + } + + @override + void initState() { + super.initState(); + if (widget.icon == Icons.favorite_outline_rounded) { + isHeartClicked = widget.isClicked; + } else if (widget.icon == Icons.star_outline_rounded) { + isFavoriteClicked = widget.isClicked; + } else if (widget.icon == Icons.check_box_outline_blank_rounded) { + isQuestionClicked = widget.isClicked; + cantClicked = isQuestionClicked; + } + textCount = int.tryParse(widget.text); + textCount ??= widget.text; + heartAnimationController = + AnimationController(vsync: this, duration: const Duration(seconds: 1)); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 4), + child: HeartAnim( + heartAnimationController: heartAnimationController, + isHeartClicked: isHeartClicked, + widget: widget, + s: 0.2, + e: -1.5, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 4), + child: HeartAnim( + heartAnimationController: heartAnimationController, + isHeartClicked: isHeartClicked, + widget: widget, + s: -0.2, + e: -1.0, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 4), + child: HeartAnim( + heartAnimationController: heartAnimationController, + isHeartClicked: isHeartClicked, + widget: widget, + s: 0.3, + e: -1.2, + ), + ), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () async { + if (widget.icon == Icons.favorite_outline_rounded) { + if (isHeartClicked) { + alreadyHeart(context); + } else { + if (widget.postId != -1) { + if (widget.isMine) { + notAllowed("자신의 글에는 좋아요를 할 수 없습니다."); + } else { + try { + ref.watch(boardAddProvider).heart(widget.postId); + increaseHeart(); + } on DioException catch (e) { + notAllowed(e.message!); + } + } + } else if (widget.commentId != -1) { + if (widget.commentId == -3) { + notAllowed("이미 삭제된 댓글입니다."); + } else if (widget.isMine) { + notAllowed("자신의 댓글에는 좋아요를 할 수 없습니다."); + } else { + final requestData = { + 'commentId': widget.commentId, + }; + try { + ref.watch(commentProvider).heart(requestData); + increaseHeart(); + } on DioException catch (e) { + notAllowed(e.message!); + } + } + } else if (widget.replyId != -1) { + if (widget.isMine) { + notAllowed("자신의 대댓글에는 좋아요를 할 수 없습니다."); + } else { + final requestData = { + 'replyId': widget.replyId, + }; + try { + ref.watch(replyProvider).heart(requestData); + increaseHeart(); + } on DioException catch (e) { + notAllowed(e.message!); + } + } + } + } + } else if (widget.icon == Icons.chat_outlined && + widget.commentId != -1) { + if (widget.commentId == -2) { + notAllowed("삭제된 댓글엔 대댓글을 달 수 없습니다."); + } else { + chatDialog(context); + } + } else if (widget.icon == Icons.star_outline_rounded) { + if (isFavoriteClicked) { + await ref.read(scrapProvider).delete(widget.postId); + } else { + await ref.read(scrapProvider).post(widget.postId); + } + ref.read(myScrapStateNotifierProvider.notifier).lastId = + 9223372036854775807; + ref + .read(myScrapStateNotifierProvider.notifier) + .paginate(forceRefetch: true); + setState(() { + if (isFavoriteClicked) { + textCount -= 1; + } else { + textCount += 1; + } + + isFavoriteClicked = !isFavoriteClicked; + }); + } else if (widget.icon == Icons.image_rounded) { + getImage().then((value) => { + if (value != "") + { + showDialog( + context: context, + builder: ((context) { + return AlertDialog( + content: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + value, + textAlign: TextAlign.center, + overflow: TextOverflow.visible, + ), + ), + ], + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () async { + openAppSettings(); + Navigator.of(context).pop(); + }, + child: const Text("확인"), + ), + ], + ), + ], + ); + })) + } + }); + } else if (widget.icon == Icons.check_box_outline_blank_rounded && + !cantClicked) { + setState(() { + isQuestionClicked = !isQuestionClicked; + }); + ref.read(isQuestionStateProvider.notifier).set(isQuestionClicked); + } else if (widget.icon == Icons.more_horiz) { + if (widget.commentId == -3) { + notAllowed("이미 삭제된 댓글입니다."); + } else if (!widget.isMine) { + // can only report + moreDialogReport(context); + } else { + moreDialog(context); + } + } + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3), + child: Row( + children: [ + Icon( + isHeartClicked + ? Icons.favorite + : isQuestionClicked + ? Icons.check_box_rounded + : isFavoriteClicked + ? Icons.star + : widget.icon, + size: widget.iconSize, + color: isHeartClicked + ? Colors.red + : isQuestionClicked + ? Colors.blue.withOpacity(0.5) + : isFavoriteClicked + ? Colors.yellow + : null, + ), + const SizedBox( + width: 2, + ), + Text( + textCount == -1 ? "" : "$textCount", + style: const TextStyle( + fontSize: 12, + ), + ), + ], + ), + ), + ), + ], + ); + } + + Future alreadyHeart(BuildContext context) { + return showDialog( + context: context, + builder: ((context) { + return AlertDialog( + content: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("이미 좋아요를 눌렀습니다."), + ], + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("확인"), + ), + ], + ), + ], + ); + })); + } + + Future chatDialog(BuildContext context) { + return showDialog( + context: context, + builder: ((context) { + return AlertDialog( + content: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("이 댓글에 대댓글을 달까요?"), + ], + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + ref + .read(commentStateProvider.notifier) + .add(0, widget.commentId); + Navigator.of(context).pop(); + }, + child: const Text("네"), + ), + const SizedBox( + width: 20, + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("아니요"), + ), + ], + ), + ], + ); + })); + } + + Future moreDialog(BuildContext context) { + return showDialog( + context: context, + builder: ((context) { + return AlertDialog( + actionsPadding: EdgeInsets.zero, + backgroundColor: PRIMARY10_COLOR, + actions: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () { + select(2); + Navigator.of(context).pop(); + }, + child: const Text( + "삭제하기", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: BODY_TEXT_COLOR.withOpacity(0.3), + width: 1, + ), + ), + ), + ), + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () { + select(1); + Navigator.of(context).pop(); + }, + child: const Text( + "수정하기", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + ], + ), + ], + ); + })); + } + + void sendReport(String reason) async { + final data = { + 'reportedObjectId': + widget.commentId != -1 ? widget.commentId : widget.replyId, + 'reportType': widget.commentId != -1 ? "COMMENT" : "REPLY", + 'reason': reason, + }; + await ref.read(reportProvider).post(data); + notAllowed("신고되었습니다."); + } + + void selectReportReason() { + showDialog( + context: context, + builder: ((context) { + return AlertDialog( + actionsPadding: EdgeInsets.zero, + backgroundColor: PRIMARY10_COLOR, + content: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + "신고 사유를 선택해주세요.", + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.black, + fontSize: 13, + fontWeight: FontWeight.bold), + ), + ), + ], + ), + actions: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () { + Navigator.of(context).pop(); + sendReport('INSULTING'); + }, + child: const Text( + "욕설/비하", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: BODY_TEXT_COLOR.withOpacity(0.3), + width: 1, + ), + ), + ), + ), + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () { + Navigator.of(context).pop(); + sendReport('COMMERCIAL'); + }, + child: const Text( + "상업적 광고 및 판매", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: BODY_TEXT_COLOR.withOpacity(0.3), + width: 1, + ), + ), + ), + ), + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () { + Navigator.of(context).pop(); + sendReport('INAPPROPRIATE'); + }, + child: const Text( + "게시판 성격에 부적절함", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: BODY_TEXT_COLOR.withOpacity(0.3), + width: 1, + ), + ), + ), + ), + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () { + Navigator.of(context).pop(); + sendReport('FRAUD'); + }, + child: const Text( + "유출/사칭/사기", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: BODY_TEXT_COLOR.withOpacity(0.3), + width: 1, + ), + ), + ), + ), + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () { + Navigator.of(context).pop(); + sendReport('SPAM'); + }, + child: const Text( + "낚시/놀람/도배", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: BODY_TEXT_COLOR.withOpacity(0.3), + width: 1, + ), + ), + ), + ), + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () { + Navigator.of(context).pop(); + sendReport('PORNOGRAPHIC'); + }, + child: const Text( + "음란물/불건전한 만남 및 대화", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + ], + ), + ], + ); + })); + } + + Future moreDialogReport(BuildContext context) { + return showDialog( + context: context, + builder: ((context) { + return AlertDialog( + actionsPadding: EdgeInsets.zero, + backgroundColor: PRIMARY10_COLOR, + actions: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () { + Navigator.of(context).pop(); + selectReportReason(); + }, + child: const Text( + "신고하기", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: BODY_TEXT_COLOR.withOpacity(0.3), + width: 1, + ), + ), + ), + ), + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () { + Navigator.of(context).pop(); + isReally(); + }, + child: const Text( + "차단하기", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + ], + ), + ], + ); + })); + } + + void isReally() { + showDialog( + context: context, + builder: ((context) { + return AlertDialog( + content: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + "이 작성자의 게시물이 목록에 노출되지 않으며, 다시 해제할 수 없습니다.", + overflow: TextOverflow.visible, + style: TextStyle( + color: Colors.black, + fontSize: 13, + ), + ), + ), + ], + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + ref.read(blockProvider).post(widget.userId); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: PRIMARY50_COLOR, + ), + child: const Text( + "확인", + style: TextStyle(fontSize: 13, color: PRIMARY_COLOR), + ), + ), + const SizedBox( + width: 10, + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: PRIMARY50_COLOR, + ), + child: const Text( + "취소", + style: TextStyle(fontSize: 13, color: PRIMARY_COLOR), + ), + ), + ], + ), + ], + ); + })); + } + + void select(int code) { + if (widget.commentId != -1) { + ref.read(commentStateProvider.notifier).add(code, widget.commentId); + } else if (widget.replyId != -1) { + ref.read(replyStateProvider.notifier).add(code, widget.replyId); + } + } +} + +class HeartAnim extends StatelessWidget { + const HeartAnim({ + super.key, + required this.heartAnimationController, + required this.isHeartClicked, + required this.widget, + required this.s, + required this.e, + }); + + final AnimationController heartAnimationController; + final bool isHeartClicked; + final TextWithIcon widget; + final double s, e; + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: Tween(begin: 3.0, end: 0.0).animate(CurvedAnimation( + parent: heartAnimationController, + curve: Curves.fastLinearToSlowEaseIn)), + child: SlideTransition( + position: + Tween(begin: const Offset(0.0, 0.0), end: Offset(s, e)) + .animate(CurvedAnimation( + parent: heartAnimationController, + curve: Curves.fastLinearToSlowEaseIn)), + child: isHeartClicked + ? Icon( + isHeartClicked ? Icons.favorite : widget.icon, + size: widget.iconSize, + color: isHeartClicked ? Colors.red : null, + ) + : null, + ), + ); + } +} diff --git a/frontend/lib/board/model/comment_model.dart b/frontend/lib/board/model/comment_model.dart new file mode 100644 index 0000000000..2374cad62f --- /dev/null +++ b/frontend/lib/board/model/comment_model.dart @@ -0,0 +1,97 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'comment_model.g.dart'; + +@JsonSerializable() +class CommentModel { + final int id; + final UserInformation userInformation; + final int userId; + final int postId; + final String content; + final LikeCount likeCount; + final bool deleted; + final bool isLiked; + final bool isBlockedUser; + final String createdAt; + final List replies; + + CommentModel( + this.id, + this.userInformation, + this.userId, + this.postId, + this.content, + this.deleted, + this.likeCount, + this.isLiked, + this.isBlockedUser, + this.createdAt, + this.replies); + + factory CommentModel.fromJson(Map json) => + _$CommentModelFromJson(json); +} + +@JsonSerializable() +class UserInformation { + final String name; + final String email; + final String nickname; + + UserInformation({ + required this.name, + required this.email, + required this.nickname, + }); + + factory UserInformation.fromJson(Map json) => + _$UserInformationFromJson(json); +} + +@JsonSerializable() +class LikeCount { + final List likes; + final int count; + + LikeCount({ + required this.likes, + required this.count, + }); + + factory LikeCount.fromJson(Map json) => + _$LikeCountFromJson(json); +} + +@JsonSerializable() +class Likes { + final int usersId; + + Likes(this.usersId); + factory Likes.fromJson(Map json) => _$LikesFromJson(json); +} + +@JsonSerializable() +class ReplyModel { + final int id; + final int userId; + final UserInformation userInformation; + final int commentId; + final String content; + final LikeCount likeCount; + final bool isBlockedUser; + final String createdAt; + + ReplyModel( + this.id, + this.userId, + this.userInformation, + this.commentId, + this.content, + this.likeCount, + this.isBlockedUser, + this.createdAt, + ); + factory ReplyModel.fromJson(Map json) => + _$ReplyModelFromJson(json); +} diff --git a/frontend/lib/board/model/comment_model.g.dart b/frontend/lib/board/model/comment_model.g.dart new file mode 100644 index 0000000000..988261bb81 --- /dev/null +++ b/frontend/lib/board/model/comment_model.g.dart @@ -0,0 +1,95 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'comment_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CommentModel _$CommentModelFromJson(Map json) => CommentModel( + (json['id'] as num).toInt(), + UserInformation.fromJson(json['userInformation'] as Map), + (json['userId'] as num).toInt(), + (json['postId'] as num).toInt(), + json['content'] as String, + json['deleted'] as bool, + LikeCount.fromJson(json['likeCount'] as Map), + json['isLiked'] as bool, + json['isBlockedUser'] as bool, + json['createdAt'] as String, + (json['replies'] as List) + .map((e) => ReplyModel.fromJson(e as Map)) + .toList(), + ); + +Map _$CommentModelToJson(CommentModel instance) => + { + 'id': instance.id, + 'userInformation': instance.userInformation, + 'userId': instance.userId, + 'postId': instance.postId, + 'content': instance.content, + 'likeCount': instance.likeCount, + 'deleted': instance.deleted, + 'isLiked': instance.isLiked, + 'isBlockedUser': instance.isBlockedUser, + 'createdAt': instance.createdAt, + 'replies': instance.replies, + }; + +UserInformation _$UserInformationFromJson(Map json) => + UserInformation( + name: json['name'] as String, + email: json['email'] as String, + nickname: json['nickname'] as String, + ); + +Map _$UserInformationToJson(UserInformation instance) => + { + 'name': instance.name, + 'email': instance.email, + 'nickname': instance.nickname, + }; + +LikeCount _$LikeCountFromJson(Map json) => LikeCount( + likes: (json['likes'] as List) + .map((e) => Likes.fromJson(e as Map)) + .toList(), + count: (json['count'] as num).toInt(), + ); + +Map _$LikeCountToJson(LikeCount instance) => { + 'likes': instance.likes, + 'count': instance.count, + }; + +Likes _$LikesFromJson(Map json) => Likes( + (json['usersId'] as num).toInt(), + ); + +Map _$LikesToJson(Likes instance) => { + 'usersId': instance.usersId, + }; + +ReplyModel _$ReplyModelFromJson(Map json) => ReplyModel( + (json['id'] as num).toInt(), + (json['userId'] as num).toInt(), + UserInformation.fromJson(json['userInformation'] as Map), + (json['commentId'] as num).toInt(), + json['content'] as String, + LikeCount.fromJson(json['likeCount'] as Map), + json['isBlockedUser'] as bool, + json['createdAt'] as String, + ); + +Map _$ReplyModelToJson(ReplyModel instance) => + { + 'id': instance.id, + 'userId': instance.userId, + 'userInformation': instance.userInformation, + 'commentId': instance.commentId, + 'content': instance.content, + 'likeCount': instance.likeCount, + 'isBlockedUser': instance.isBlockedUser, + 'createdAt': instance.createdAt, + }; diff --git a/frontend/lib/board/model/comment_response_model.dart b/frontend/lib/board/model/comment_response_model.dart new file mode 100644 index 0000000000..7fe48441f6 --- /dev/null +++ b/frontend/lib/board/model/comment_response_model.dart @@ -0,0 +1,19 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'comment_response_model.g.dart'; + +@JsonSerializable() +class CommentResponseModel { + final int id; + final int userId; + final int postId; + final String content; + final String createdDateTime; + final String modifiedDateTime; + + CommentResponseModel(this.id, this.userId, this.postId, this.content, + this.createdDateTime, this.modifiedDateTime); + + factory CommentResponseModel.fromJson(Map json) => + _$CommentResponseModelFromJson(json); +} diff --git a/frontend/lib/board/model/comment_response_model.g.dart b/frontend/lib/board/model/comment_response_model.g.dart new file mode 100644 index 0000000000..901238512f --- /dev/null +++ b/frontend/lib/board/model/comment_response_model.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'comment_response_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CommentResponseModel _$CommentResponseModelFromJson( + Map json) => + CommentResponseModel( + (json['id'] as num).toInt(), + (json['userId'] as num).toInt(), + (json['postId'] as num).toInt(), + json['content'] as String, + json['createdDateTime'] as String, + json['modifiedDateTime'] as String, + ); + +Map _$CommentResponseModelToJson( + CommentResponseModel instance) => + { + 'id': instance.id, + 'userId': instance.userId, + 'postId': instance.postId, + 'content': instance.content, + 'createdDateTime': instance.createdDateTime, + 'modifiedDateTime': instance.modifiedDateTime, + }; diff --git a/frontend/lib/board/model/exception_model.dart b/frontend/lib/board/model/exception_model.dart new file mode 100644 index 0000000000..6da3405b18 --- /dev/null +++ b/frontend/lib/board/model/exception_model.dart @@ -0,0 +1,14 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'exception_model.g.dart'; + +@JsonSerializable() +class ExceptionModel { + final String code; + final String message; + + factory ExceptionModel.fromJson(Map json) => + _$ExceptionModelFromJson(json); + + ExceptionModel(this.code, this.message); +} diff --git a/frontend/lib/board/model/exception_model.g.dart b/frontend/lib/board/model/exception_model.g.dart new file mode 100644 index 0000000000..dbc79cbfc7 --- /dev/null +++ b/frontend/lib/board/model/exception_model.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'exception_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ExceptionModel _$ExceptionModelFromJson(Map json) => + ExceptionModel( + json['code'] as String, + json['message'] as String, + ); + +Map _$ExceptionModelToJson(ExceptionModel instance) => + { + 'code': instance.code, + 'message': instance.message, + }; diff --git a/frontend/lib/board/model/msg_board_detail_response_model.dart b/frontend/lib/board/model/msg_board_detail_response_model.dart new file mode 100644 index 0000000000..d531c22846 --- /dev/null +++ b/frontend/lib/board/model/msg_board_detail_response_model.dart @@ -0,0 +1,32 @@ +import 'package:frontend/board/model/msg_board_response_model.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'msg_board_detail_response_model.g.dart'; + +@JsonSerializable() +class MsgBoardDetailResponseModel extends MsgBoardResponseModel { + final bool isScrapped; + final bool likedBy; + + MsgBoardDetailResponseModel({ + required super.id, + required super.userId, + required super.userNickname, + required super.universityName, + required super.communityId, + required super.communityTitle, + required super.postTitle, + required super.postContent, + required super.images, + required super.count, + required super.isQuestion, + required super.isBlockedUser, + required this.isScrapped, + required this.likedBy, + required super.createdDateTime, + required super.imageCount, + }); + + factory MsgBoardDetailResponseModel.fromJson(Map json) => + _$MsgBoardDetailResponseModelFromJson(json); +} diff --git a/frontend/lib/board/model/msg_board_detail_response_model.g.dart b/frontend/lib/board/model/msg_board_detail_response_model.g.dart new file mode 100644 index 0000000000..d9062c8413 --- /dev/null +++ b/frontend/lib/board/model/msg_board_detail_response_model.g.dart @@ -0,0 +1,50 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'msg_board_detail_response_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +MsgBoardDetailResponseModel _$MsgBoardDetailResponseModelFromJson( + Map json) => + MsgBoardDetailResponseModel( + id: (json['id'] as num).toInt(), + userId: (json['userId'] as num).toInt(), + userNickname: json['userNickname'] as String, + universityName: json['universityName'] as String, + communityId: (json['communityId'] as num).toInt(), + communityTitle: json['communityTitle'] as String, + postTitle: json['postTitle'] as String, + postContent: json['postContent'] as String, + images: + (json['images'] as List).map((e) => e as String).toList(), + count: ReactCountModel.fromJson(json['count'] as Map), + isQuestion: json['isQuestion'] as bool, + isBlockedUser: json['isBlockedUser'] as bool, + isScrapped: json['isScrapped'] as bool, + likedBy: json['likedBy'] as bool, + createdDateTime: json['createdDateTime'] as String, + imageCount: (json['imageCount'] as num).toInt(), + ); + +Map _$MsgBoardDetailResponseModelToJson( + MsgBoardDetailResponseModel instance) => + { + 'id': instance.id, + 'userId': instance.userId, + 'userNickname': instance.userNickname, + 'universityName': instance.universityName, + 'communityId': instance.communityId, + 'communityTitle': instance.communityTitle, + 'postTitle': instance.postTitle, + 'postContent': instance.postContent, + 'images': instance.images, + 'count': instance.count, + 'isQuestion': instance.isQuestion, + 'isBlockedUser': instance.isBlockedUser, + 'imageCount': instance.imageCount, + 'createdDateTime': instance.createdDateTime, + 'isScrapped': instance.isScrapped, + 'likedBy': instance.likedBy, + }; diff --git a/frontend/lib/board/model/msg_board_model.dart b/frontend/lib/board/model/msg_board_model.dart new file mode 100644 index 0000000000..f78778d798 --- /dev/null +++ b/frontend/lib/board/model/msg_board_model.dart @@ -0,0 +1,22 @@ +class MsgBoardModel { + final String id, + postId, + category, + title, + preview, + heart, + comment, + favorite, + date, + name; + + MsgBoardModel(this.id, this.postId, this.category, this.title, this.preview, + this.heart, this.comment, this.favorite, this.date, this.name); + // MsgBoardListModel.fromJson(Map json) + // : id = json['id'], + // category = json['category'], + // title = json['title'], + // heart = json['heart'], + // comment = json['comment'], + // favorite = json['favorite']; +} diff --git a/frontend/lib/board/model/msg_board_response_model.dart b/frontend/lib/board/model/msg_board_response_model.dart new file mode 100644 index 0000000000..637918caf0 --- /dev/null +++ b/frontend/lib/board/model/msg_board_response_model.dart @@ -0,0 +1,57 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'msg_board_response_model.g.dart'; + +@JsonSerializable() +class MsgBoardResponseModel { + final int id; + final int userId; + final String userNickname; + final String universityName; + final int communityId; + final String communityTitle; + final String postTitle; + final String postContent; + final List images; + final ReactCountModel count; + final bool isQuestion; + final bool isBlockedUser; + final int imageCount; + final String createdDateTime; + + MsgBoardResponseModel({ + required this.id, + required this.userId, + required this.userNickname, + required this.universityName, + required this.communityId, + required this.communityTitle, + required this.postTitle, + required this.postContent, + required this.images, + required this.count, + required this.isQuestion, + required this.isBlockedUser, + required this.imageCount, + required this.createdDateTime, + }); + + factory MsgBoardResponseModel.fromJson(Map json) => + _$MsgBoardResponseModelFromJson(json); +} + +@JsonSerializable() +class ReactCountModel { + final int commentReplyCount; + final int likeCount; + final int scrapCount; + + ReactCountModel({ + required this.commentReplyCount, + required this.likeCount, + required this.scrapCount, + }); + + factory ReactCountModel.fromJson(Map json) => + _$ReactCountModelFromJson(json); +} diff --git a/frontend/lib/board/model/msg_board_response_model.g.dart b/frontend/lib/board/model/msg_board_response_model.g.dart new file mode 100644 index 0000000000..308bffe05f --- /dev/null +++ b/frontend/lib/board/model/msg_board_response_model.g.dart @@ -0,0 +1,60 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'msg_board_response_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +MsgBoardResponseModel _$MsgBoardResponseModelFromJson( + Map json) => + MsgBoardResponseModel( + id: (json['id'] as num).toInt(), + userId: (json['userId'] as num).toInt(), + userNickname: json['userNickname'] as String, + universityName: json['universityName'] as String, + communityId: (json['communityId'] as num).toInt(), + communityTitle: json['communityTitle'] as String, + postTitle: json['postTitle'] as String, + postContent: json['postContent'] as String, + images: + (json['images'] as List).map((e) => e as String).toList(), + count: ReactCountModel.fromJson(json['count'] as Map), + isQuestion: json['isQuestion'] as bool, + isBlockedUser: json['isBlockedUser'] as bool, + imageCount: (json['imageCount'] as num).toInt(), + createdDateTime: json['createdDateTime'] as String, + ); + +Map _$MsgBoardResponseModelToJson( + MsgBoardResponseModel instance) => + { + 'id': instance.id, + 'userId': instance.userId, + 'userNickname': instance.userNickname, + 'universityName': instance.universityName, + 'communityId': instance.communityId, + 'communityTitle': instance.communityTitle, + 'postTitle': instance.postTitle, + 'postContent': instance.postContent, + 'images': instance.images, + 'count': instance.count, + 'isQuestion': instance.isQuestion, + 'isBlockedUser': instance.isBlockedUser, + 'imageCount': instance.imageCount, + 'createdDateTime': instance.createdDateTime, + }; + +ReactCountModel _$ReactCountModelFromJson(Map json) => + ReactCountModel( + commentReplyCount: (json['commentReplyCount'] as num).toInt(), + likeCount: (json['likeCount'] as num).toInt(), + scrapCount: (json['scrapCount'] as num).toInt(), + ); + +Map _$ReactCountModelToJson(ReactCountModel instance) => + { + 'commentReplyCount': instance.commentReplyCount, + 'likeCount': instance.likeCount, + 'scrapCount': instance.scrapCount, + }; diff --git a/frontend/lib/board/model/notification_model.dart b/frontend/lib/board/model/notification_model.dart new file mode 100644 index 0000000000..8bf6949cdd --- /dev/null +++ b/frontend/lib/board/model/notification_model.dart @@ -0,0 +1,5 @@ +class NotificationModel { + DateTime lastHeartbeat; + int retryCount; + NotificationModel(this.lastHeartbeat, this.retryCount); +} diff --git a/frontend/lib/board/provider/api_category_provider.dart b/frontend/lib/board/provider/api_category_provider.dart new file mode 100644 index 0000000000..01733ac760 --- /dev/null +++ b/frontend/lib/board/provider/api_category_provider.dart @@ -0,0 +1,6 @@ +// board에서 선택한 게시판과 관련된 data들을 관리하는 provider들 +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final categoryTitleProvider = StateProvider((ref) => null); + +final isHotProvider = StateProvider((ref) => false); \ No newline at end of file diff --git a/frontend/lib/board/provider/block_provider.dart b/frontend/lib/board/provider/block_provider.dart new file mode 100644 index 0000000000..ff1021ead7 --- /dev/null +++ b/frontend/lib/board/provider/block_provider.dart @@ -0,0 +1,27 @@ +import 'package:dio/dio.dart' hide Headers; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/common/provider/dio_provider.dart'; +import 'package:retrofit/http.dart'; + +import '../../common/const/data.dart'; + +part 'block_provider.g.dart'; + +final blockProvider = Provider((ref) { + final dio = ref.watch(dioProvider); + + return Block(dio, baseUrl: ip); +}); + +@RestApi() +abstract class Block { + factory Block(Dio dio, {String baseUrl}) = _Block; + + @POST('/api/block') + @Headers({ + 'accessToken': 'true', + }) + Future post( + @Query('blockedUserId') int blockedUserId, + ); +} diff --git a/frontend/lib/board/provider/block_provider.g.dart b/frontend/lib/board/provider/block_provider.g.dart new file mode 100644 index 0000000000..9ea58e8123 --- /dev/null +++ b/frontend/lib/board/provider/block_provider.g.dart @@ -0,0 +1,75 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'block_provider.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers + +class _Block implements Block { + _Block( + this._dio, { + this.baseUrl, + }); + + final Dio _dio; + + String? baseUrl; + + @override + Future post(int blockedUserId) async { + const _extra = {}; + final queryParameters = {r'blockedUserId': blockedUserId}; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final Map? _data = null; + await _dio.fetch(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/block', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/frontend/lib/board/provider/board_add_provider.dart b/frontend/lib/board/provider/board_add_provider.dart new file mode 100644 index 0000000000..af7a87e5b6 --- /dev/null +++ b/frontend/lib/board/provider/board_add_provider.dart @@ -0,0 +1,61 @@ +import 'package:dio/dio.dart' hide Headers; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/board/model/msg_board_detail_response_model.dart'; +import 'package:frontend/common/provider/dio_provider.dart'; +import 'package:retrofit/http.dart'; + +import '../../common/const/data.dart'; +import '../model/msg_board_response_model.dart'; + +part 'board_add_provider.g.dart'; + +final boardAddProvider = Provider((ref) { + final dio = ref.watch(dioProvider); + + return BoardAdd(dio, baseUrl: ip); +}); + +@RestApi() +abstract class BoardAdd { + factory BoardAdd(Dio dio, {String baseUrl}) = _BoardAdd; + + @POST('/api/post') + @Headers({ + 'accessToken': 'true', + }) + Future post( + @Body() Map data, + ); + + @PUT('/api/post/{postId}') + @Headers({ + 'accessToken': 'true', + }) + Future heart( + @Path('postId') int postId, + ); + + @DELETE('/api/post/{postId}') + @Headers({ + 'accessToken': 'true', + }) + Future delete( + @Path('postId') int postId, + ); + + @PUT('/api/post') + @Headers({ + 'accessToken': 'true', + }) + Future modify( + @Body() Map data, + ); + + @GET('/api/post/{postId}') + @Headers({ + 'accessToken': 'true', + }) + Future get( + @Path('postId') int postId, + ); +} diff --git a/frontend/lib/board/provider/board_add_provider.g.dart b/frontend/lib/board/provider/board_add_provider.g.dart new file mode 100644 index 0000000000..34b3560be2 --- /dev/null +++ b/frontend/lib/board/provider/board_add_provider.g.dart @@ -0,0 +1,186 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'board_add_provider.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers + +class _BoardAdd implements BoardAdd { + _BoardAdd( + this._dio, { + this.baseUrl, + }); + + final Dio _dio; + + String? baseUrl; + + @override + Future post(Map data) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final _data = {}; + _data.addAll(data); + final _result = await _dio.fetch>( + _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/post', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = MsgBoardResponseModel.fromJson(_result.data!); + return value; + } + + @override + Future heart(int postId) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final Map? _data = null; + await _dio.fetch(_setStreamType(Options( + method: 'PUT', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/post/${postId}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + } + + @override + Future delete(int postId) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final Map? _data = null; + await _dio.fetch(_setStreamType(Options( + method: 'DELETE', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/post/${postId}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + } + + @override + Future modify(Map data) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final _data = {}; + _data.addAll(data); + final _result = await _dio.fetch>( + _setStreamType(Options( + method: 'PUT', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/post', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = MsgBoardResponseModel.fromJson(_result.data!); + return value; + } + + @override + Future get(int postId) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final Map? _data = null; + final _result = await _dio.fetch>( + _setStreamType(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/post/${postId}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = MsgBoardDetailResponseModel.fromJson(_result.data!); + return value; + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/frontend/lib/board/provider/board_detail_state_notifier_provider.dart b/frontend/lib/board/provider/board_detail_state_notifier_provider.dart new file mode 100644 index 0000000000..2b44d97f37 --- /dev/null +++ b/frontend/lib/board/provider/board_detail_state_notifier_provider.dart @@ -0,0 +1,16 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class BoardDetailNotifier extends StateNotifier { + BoardDetailNotifier(this.ref) : super(-1); + + final Ref ref; + + Future add(int postId) async { + state = postId; + } +} + +final boardDetailNotifier = + StateNotifierProvider((ref) { + return BoardDetailNotifier(ref); +}); diff --git a/frontend/lib/board/provider/board_repository_provider.dart b/frontend/lib/board/provider/board_repository_provider.dart new file mode 100644 index 0000000000..02f0667cd9 --- /dev/null +++ b/frontend/lib/board/provider/board_repository_provider.dart @@ -0,0 +1,31 @@ +import 'package:dio/dio.dart' hide Headers; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/common/model/cursor_pagination_model.dart'; +import 'package:frontend/common/provider/dio_provider.dart'; +import 'package:retrofit/http.dart'; + +import '../../common/const/data.dart'; +import '../model/msg_board_response_model.dart'; + +part 'board_repository_provider.g.dart'; + +final boardRepositoryProvider = Provider((ref) { + final dio = ref.watch(dioProvider); + + return BoardRepository(dio, baseUrl: ip); +}); + +@RestApi() +abstract class BoardRepository { + factory BoardRepository(Dio dio, {String baseUrl}) = _BoardRepository; + + @GET('/api/post') + @Headers({ + 'accessToken': 'true', + }) + Future> paginate( + @Query('lastId') int lastId, + @Query('communityTitle') String? communityTitle, + @Query('size') int size, + @Query('isHot') bool isHot); +} diff --git a/frontend/lib/board/provider/board_repository_provider.g.dart b/frontend/lib/board/provider/board_repository_provider.g.dart new file mode 100644 index 0000000000..117e68d451 --- /dev/null +++ b/frontend/lib/board/provider/board_repository_provider.g.dart @@ -0,0 +1,92 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'board_repository_provider.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers + +class _BoardRepository implements BoardRepository { + _BoardRepository( + this._dio, { + this.baseUrl, + }); + + final Dio _dio; + + String? baseUrl; + + @override + Future> paginate( + int lastId, + String? communityTitle, + int size, + bool isHot, + ) async { + const _extra = {}; + final queryParameters = { + r'lastId': lastId, + r'communityTitle': communityTitle, + r'size': size, + r'isHot': isHot, + }; + queryParameters.removeWhere((k, v) => v == null); + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final Map? _data = null; + final _result = await _dio.fetch>( + _setStreamType>(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/post', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = CursorPaginationModel.fromJson( + _result.data!, + (json) => MsgBoardResponseModel.fromJson(json as Map), + ); + return value; + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/frontend/lib/board/provider/board_state_notifier_provider.dart b/frontend/lib/board/provider/board_state_notifier_provider.dart new file mode 100644 index 0000000000..b33396a844 --- /dev/null +++ b/frontend/lib/board/provider/board_state_notifier_provider.dart @@ -0,0 +1,128 @@ +import 'dart:core'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/board/provider/board_repository_provider.dart'; +import 'package:frontend/common/model/cursor_pagination_model.dart'; + +import 'api_category_provider.dart'; + +final boardStateNotifierProvider = + StateNotifierProvider( + (ref) { + final repository = ref.watch(boardRepositoryProvider); + const initialLastPostId = 9223372036854775807; + final communityTitle = ref.watch(categoryTitleProvider); + final isHot = ref.watch(isHotProvider); + const size = 20; + + final notifier = BoardStateNotifier( + repository: repository, + lastId: initialLastPostId, + communityTitle: communityTitle, + size: size, + isHot: isHot); + return notifier; + }, +); + +class BoardStateNotifier extends StateNotifier { + bool _mounted = true; + bool _fetchingData = false; + + @override + void dispose() { + _mounted = false; + super.dispose(); + } + + final BoardRepository repository; + int lastId; + String? communityTitle; + int size; + bool isHot; + + BoardStateNotifier({ + required this.repository, + required this.lastId, + this.communityTitle, + required this.size, + required this.isHot, + }) : super(CursorPaginationModelLoading()) { + paginate(); + } + + bool get isMounted => _mounted; + + Future paginate({ + bool fetchMore = false, + bool forceRefetch = false, + }) async { + if (!isMounted) return; + + if (_fetchingData) return; + _fetchingData = true; + + try { + if (state is CursorPaginationModel && !forceRefetch) { + final pState = state as CursorPaginationModel; + + if (!pState.meta.hasMore) { + return; + } + } + + final isLoading = state is CursorPaginationModelLoading; + final isRefetching = state is CursorPaginationModelRefetching; + final isFetchingMore = state is CursorPaginationModelFetchingMore; + + if (fetchMore && (isLoading || isRefetching || isFetchingMore)) { + return; + } + + if (fetchMore) { + final pState = (state as CursorPaginationModel); // 무조건 데이터를 들고있는 상황 + + state = CursorPaginationModelFetchingMore( + meta: pState.meta, + data: pState.data, + ); + lastId = pState.data.last.id; + } else { + if (state is CursorPaginationModel && !forceRefetch) { + final pState = state as CursorPaginationModel; + + state = CursorPaginationModelRefetching( + meta: pState.meta, + data: pState.data, + ); + } else { + state = CursorPaginationModelLoading(); + } + } + final resp = + await repository.paginate(lastId, communityTitle, size, isHot); + + if (!isMounted) return; + + if (state is CursorPaginationModelFetchingMore) { + final pState = state as CursorPaginationModelFetchingMore; + state = resp.copyWith( + data: [ + ...pState.data, + ...resp.data, + ], + ); + } else { + state = resp; + } + } catch (e) { + if (!isMounted) return; + + print(e.runtimeType); + + state = CursorPaginationModelError(message: '데이터를 가져오지 못했습니다'); + } finally { + _fetchingData = false; + } + } +} diff --git a/frontend/lib/board/provider/category_provider.dart b/frontend/lib/board/provider/category_provider.dart new file mode 100644 index 0000000000..961d8af4e7 --- /dev/null +++ b/frontend/lib/board/provider/category_provider.dart @@ -0,0 +1,24 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class CategoryNotifier extends StateNotifier> { + CategoryNotifier(this.ref) : super(["전체"]); + + final Ref ref; + + Future add(String category) async { + state = [...state, category]; + } + + Future remove(String category) async { + state = List.from(state)..remove(category); + } + + Future clear() async { + state = List.from(state)..clear(); + } +} + +final categoryStateProvider = + StateNotifierProvider>((ref) { + return CategoryNotifier(ref); +}); diff --git a/frontend/lib/board/provider/cloudwatch_provider.dart b/frontend/lib/board/provider/cloudwatch_provider.dart new file mode 100644 index 0000000000..df57c1e13a --- /dev/null +++ b/frontend/lib/board/provider/cloudwatch_provider.dart @@ -0,0 +1,38 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:aws_cloudwatch/aws_cloudwatch.dart'; +import 'package:frontend/common/const/ip_list.dart'; +import 'package:intl/intl.dart'; + +class CloudWatchNotifier extends StateNotifier { + CloudWatchNotifier(this.ref) : super(""); + + final Ref ref; + CloudWatchHandler logging = CloudWatchHandler( + awsAccessKey: awsAccessKeyId, + awsSecretKey: awsSecretAccessKey, + region: "ap-northeast-2", + delay: const Duration(milliseconds: 200), + ); + + Future add(String logString) async { + String logStreamName = DateFormat('yyyy-MM-dd HH-mm-ss').format( + DateTime.now().toUtc(), + ); + logging.log( + message: logString, + logGroupName: "Error", + logStreamName: logStreamName, + ); + + state = logString; + } + + Future clear() async { + state = ""; + } +} + +final cloudWatchStateProvider = + StateNotifierProvider((ref) { + return CloudWatchNotifier(ref); +}); diff --git a/frontend/lib/board/provider/comment_notifier_provider.dart b/frontend/lib/board/provider/comment_notifier_provider.dart new file mode 100644 index 0000000000..aee62d2363 --- /dev/null +++ b/frontend/lib/board/provider/comment_notifier_provider.dart @@ -0,0 +1,25 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class CommentNotifier extends StateNotifier> { + CommentNotifier(this.ref) : super([-1, -1, -1]); + + final Ref ref; + + Future add(int type, int index) async { + if (type == 0) { + // add comment + state = [index, -1, -1]; + } else if (type == 1) { + // modify comment + state = [-1, index, -1]; + } else { + // delete comment + state = [-1, -1, index]; + } + } +} + +final commentStateProvider = + StateNotifierProvider>((ref) { + return CommentNotifier(ref); +}); diff --git a/frontend/lib/board/provider/comment_pagination_provider.dart b/frontend/lib/board/provider/comment_pagination_provider.dart new file mode 100644 index 0000000000..781accdd12 --- /dev/null +++ b/frontend/lib/board/provider/comment_pagination_provider.dart @@ -0,0 +1,124 @@ +import 'dart:core'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/board/provider/board_detail_state_notifier_provider.dart'; +import 'package:frontend/board/provider/comment_provider.dart'; +import 'package:frontend/common/model/cursor_pagination_model.dart'; + +final commentPaginationProvider = + StateNotifierProvider( + (ref) { + final commentNotifier = ref.watch(commentProvider); + final postId = ref.watch(boardDetailNotifier); + const initialLastPostId = 1; + const size = 15; + + final notifier = CommentPaginationNotifier( + commentNotifier: commentNotifier, + postId: postId, + lastCommentId: initialLastPostId, + size: size, + ); + return notifier; + }, +); + +class CommentPaginationNotifier + extends StateNotifier { + bool _mounted = true; + bool _fetchingData = false; + + @override + void dispose() { + _mounted = false; + super.dispose(); + } + + final CommentNotifier commentNotifier; + int postId; + int lastCommentId; + int size; + + CommentPaginationNotifier({ + required this.commentNotifier, + required this.postId, + required this.lastCommentId, + required this.size, + }) : super(CursorPaginationModelLoading()) { + paginate(); + } + + bool get isMounted => _mounted; + + Future paginate({ + bool fetchMore = false, + bool forceRefetch = false, + }) async { + if (!isMounted) return; + + if (_fetchingData) return; + _fetchingData = true; + + try { + if (state is CursorPaginationModel && !forceRefetch) { + final pState = state as CursorPaginationModel; + + if (!pState.meta.hasMore) { + return; + } + } + final isLoading = state is CursorPaginationModelLoading; + final isRefetching = state is CursorPaginationModelRefetching; + final isFetchingMore = state is CursorPaginationModelFetchingMore; + + if (fetchMore && (isLoading || isRefetching || isFetchingMore)) { + return; + } + + if (fetchMore) { + final pState = (state as CursorPaginationModel); // 무조건 데이터를 들고있는 상황 + + state = CursorPaginationModelFetchingMore( + meta: pState.meta, + data: pState.data, + ); + lastCommentId = pState.data.last.id; + } else { + if (state is CursorPaginationModel && !forceRefetch) { + final pState = state as CursorPaginationModel; + state = CursorPaginationModelRefetching( + meta: pState.meta, + data: pState.data, + ); + } else { + state = CursorPaginationModelLoading(); + } + lastCommentId = 1; + } + final resp = await commentNotifier.paginate(postId, lastCommentId, size); + + if (!isMounted) return; + + if (state is CursorPaginationModelFetchingMore) { + final pState = state as CursorPaginationModelFetchingMore; + state = resp.copyWith( + data: [ + ...pState.data, + ...resp.data, + ], + ); + } else { + state = resp; + } + } catch (e) { + if (!isMounted) return; + + debugPrint("CommentPaginationError : ${e.runtimeType.toString()}"); + + state = CursorPaginationModelError(message: '데이터를 가져오지 못했습니다'); + } finally { + _fetchingData = false; + } + } +} diff --git a/frontend/lib/board/provider/comment_provider.dart b/frontend/lib/board/provider/comment_provider.dart new file mode 100644 index 0000000000..1ce8b720e8 --- /dev/null +++ b/frontend/lib/board/provider/comment_provider.dart @@ -0,0 +1,64 @@ +import 'package:dio/dio.dart' hide Headers; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/board/model/comment_model.dart'; +import 'package:frontend/board/model/comment_response_model.dart'; +import 'package:frontend/common/const/data.dart'; +import 'package:frontend/common/model/cursor_pagination_model.dart'; +import 'package:frontend/common/provider/dio_provider.dart'; +import 'package:retrofit/http.dart'; + +part 'comment_provider.g.dart'; + +final commentProvider = Provider((ref) { + final dio = ref.watch(dioProvider); + + return CommentNotifier(dio, baseUrl: ip); +}); + +@RestApi() +abstract class CommentNotifier { + factory CommentNotifier(Dio dio, {String baseUrl}) = _CommentNotifier; + + @GET('/api/comments') + @Headers({ + 'accessToken': 'true', + }) + Future> paginate( + @Query('postId') int postId, + @Query('lastCommentId') int lastCommentId, + @Query('size') int size, + ); + + @POST('/api/comments') + @Headers({ + 'accessToken': 'true', + }) + Future post( + @Body() Map data, + ); + + @POST('/api/comments/likes') + @Headers({ + 'accessToken': 'true', + }) + Future heart( + @Body() Map data, + ); + + @PUT('/api/comments/{commentId}') + @Headers({ + 'accessToken': 'true', + }) + Future modify( + @Path() int commentId, + @Body() Map data, + ); + + @DELETE('/api/comments/{commentId}') + @Headers({ + 'accessToken': 'true', + }) + Future delete( + @Path() int commentId, + ); +} diff --git a/frontend/lib/board/provider/comment_provider.g.dart b/frontend/lib/board/provider/comment_provider.g.dart new file mode 100644 index 0000000000..339319b255 --- /dev/null +++ b/frontend/lib/board/provider/comment_provider.g.dart @@ -0,0 +1,198 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'comment_provider.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers + +class _CommentNotifier implements CommentNotifier { + _CommentNotifier( + this._dio, { + this.baseUrl, + }); + + final Dio _dio; + + String? baseUrl; + + @override + Future> paginate( + int postId, + int lastCommentId, + int size, + ) async { + const _extra = {}; + final queryParameters = { + r'postId': postId, + r'lastCommentId': lastCommentId, + r'size': size, + }; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final Map? _data = null; + final _result = await _dio.fetch>( + _setStreamType>(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/comments', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = CursorPaginationModel.fromJson( + _result.data!, + (json) => CommentModel.fromJson(json as Map), + ); + return value; + } + + @override + Future post(Map data) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final _data = {}; + _data.addAll(data); + final _result = await _dio.fetch>( + _setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/comments', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = CommentResponseModel.fromJson(_result.data!); + return value; + } + + @override + Future heart(Map data) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final _data = {}; + _data.addAll(data); + await _dio.fetch(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/comments/likes', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + } + + @override + Future modify( + int commentId, + Map data, + ) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final _data = {}; + _data.addAll(data); + await _dio.fetch(_setStreamType(Options( + method: 'PUT', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/comments/${commentId}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + } + + @override + Future delete(int commentId) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final Map? _data = null; + await _dio.fetch(_setStreamType(Options( + method: 'DELETE', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/comments/${commentId}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/frontend/lib/board/provider/image_provider.dart b/frontend/lib/board/provider/image_provider.dart new file mode 100644 index 0000000000..adcd6a558a --- /dev/null +++ b/frontend/lib/board/provider/image_provider.dart @@ -0,0 +1,25 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; + +class ImageNotifier extends StateNotifier> { + ImageNotifier(this.ref) : super([]); + + final Ref ref; + + Future add(List images) async { + state = images; + } + + Future clear() async { + state = List.from(state)..clear(); + } + + Future remove(XFile image) async { + state = state.where((item) => item != image).toList(); + } +} + +final imageStateProvider = + StateNotifierProvider>((ref) { + return ImageNotifier(ref); +}); diff --git a/frontend/lib/board/provider/isquestion_provider.dart b/frontend/lib/board/provider/isquestion_provider.dart new file mode 100644 index 0000000000..e0feb5d752 --- /dev/null +++ b/frontend/lib/board/provider/isquestion_provider.dart @@ -0,0 +1,16 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class IsQuestionNotifier extends StateNotifier { + IsQuestionNotifier(this.ref) : super(false); + + final Ref ref; + + Future set(bool isClicked) async { + state = isClicked; + } +} + +final isQuestionStateProvider = + StateNotifierProvider((ref) { + return IsQuestionNotifier(ref); +}); diff --git a/frontend/lib/board/provider/network_image_provider.dart b/frontend/lib/board/provider/network_image_provider.dart new file mode 100644 index 0000000000..dd69951162 --- /dev/null +++ b/frontend/lib/board/provider/network_image_provider.dart @@ -0,0 +1,24 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class NetworkImageNotifier extends StateNotifier> { + NetworkImageNotifier(this.ref) : super([]); + + final Ref ref; + + Future add(String image) async { + state = List.from(state)..add(image); + } + + Future clear() async { + state = List.from(state)..clear(); + } + + Future remove(String image) async { + state = state.where((item) => item != image).toList(); + } +} + +final networkImageStateProvider = + StateNotifierProvider>((ref) { + return NetworkImageNotifier(ref); +}); diff --git a/frontend/lib/board/provider/notification_notifier_provider.dart b/frontend/lib/board/provider/notification_notifier_provider.dart new file mode 100644 index 0000000000..da2192e7ca --- /dev/null +++ b/frontend/lib/board/provider/notification_notifier_provider.dart @@ -0,0 +1,137 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_client_sse/constants/sse_request_type_enum.dart'; +import 'package:flutter_client_sse/flutter_client_sse.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:frontend/board/model/notification_model.dart'; +import 'package:frontend/board/provider/payload_state_notifier_provider.dart'; +import 'package:frontend/common/const/data.dart'; +import 'package:frontend/common/provider/secure_storage_provider.dart'; +import 'package:flutter_local_notifications/src/platform_specifics/android/enums.dart' + as noti; +import 'package:permission_handler/permission_handler.dart'; + +class NotificationNotifier extends StateNotifier { + NotificationNotifier(this.ref, this.storage) + : super(NotificationModel(DateTime.now(), 0)); + + final Ref ref; + final FlutterSecureStorage storage; + final FlutterLocalNotificationsPlugin notification = + FlutterLocalNotificationsPlugin(); + + Future initNotification() async { + AndroidInitializationSettings android = + const AndroidInitializationSettings("@mipmap/ic_launcher"); + DarwinInitializationSettings ios = const DarwinInitializationSettings( + requestSoundPermission: false, + requestBadgePermission: false, + requestAlertPermission: false, + ); + InitializationSettings settings = + InitializationSettings(android: android, iOS: ios); + await notification.initialize( + settings, + onDidReceiveNotificationResponse: (details) { + if (details.payload != null || details.payload!.isNotEmpty) { + // TODO : Add payload State Notifier + ref.read(payloadNotifier.notifier).add(details.payload!); + debugPrint("Foreground Payload : ${details.payload}"); + } + }, + ); + } + + void requestNotificationPermission() { + Permission.notification.isDenied.then((value) { + if (value) { + Permission.notification.request(); + } + }); + notification + .resolvePlatformSpecificImplementation< + IOSFlutterLocalNotificationsPlugin>() + ?.requestPermissions( + alert: true, + badge: true, + sound: true, + ); + } + + void sendNotification(String title, String body, int postId) { + NotificationDetails details = const NotificationDetails( + iOS: DarwinNotificationDetails( + badgeNumber: 1, + presentAlert: true, + presentBadge: true, + presentSound: true, + ), + android: AndroidNotificationDetails( + "1", + "test", + importance: Importance.max, + priority: noti.Priority.high, + ), + ); + + notification.show(0, title, body, details, payload: "$postId"); + } + + Future listen(int retryCount) async { + if (retryCount == 0) { + await initNotification(); + requestNotificationPermission(); + } + + if (retryCount >= 3) { + return; + } + + final accessToken = await storage.read(key: ACCESS_TOKEN_KEY); + DateTime lastHeartbeat = DateTime.now(); + int heartbeatCount = 0; + + SSEClient.subscribeToSSE( + method: SSERequestType.GET, + url: "$ip/api/notifications/subscribe", + header: { + "Authorization": "Bearer $accessToken", + "Accept": "text/event-stream" + }).listen((event) { + debugPrint("SSE : ${event.event}, ${event.data}"); + String e = event.data ?? ""; + if (e != "" && + !e.contains("EventStream Created.") && + !e.contains("heartbeat")) { + Map response = jsonDecode(e); + sendNotification( + response["type"], response["content"], response["postId"]); + } else if (e.contains("heartbeat")) { + lastHeartbeat = DateTime.now(); + heartbeatCount += 1; + } + + state = NotificationModel(lastHeartbeat, retryCount); + + if (heartbeatCount > 18) { + SSEClient.unsubscribeFromSSE(); + } + }).onError((e) async { + debugPrint("SSE-Error : ${e.toString()}"); + final accessToken = await storage.read(key: ACCESS_TOKEN_KEY); + final refreshToken = await storage.read(key: REFRESH_TOKEN_KEY); + + if (refreshToken != null && accessToken != null) { + listen(retryCount + 1); + } + }); + } +} + +final notificationStateProvider = + StateNotifierProvider((ref) { + return NotificationNotifier(ref, ref.watch(secureStorageProvider)); +}); diff --git a/frontend/lib/board/provider/payload_state_notifier_provider.dart b/frontend/lib/board/provider/payload_state_notifier_provider.dart new file mode 100644 index 0000000000..b102d43386 --- /dev/null +++ b/frontend/lib/board/provider/payload_state_notifier_provider.dart @@ -0,0 +1,15 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class PayloadNotifier extends StateNotifier { + PayloadNotifier(this.ref) : super(""); + + final Ref ref; + + Future add(String postId) async { + state = postId; + } +} + +final payloadNotifier = StateNotifierProvider((ref) { + return PayloadNotifier(ref); +}); diff --git a/frontend/lib/board/provider/reply_notifier_provider.dart b/frontend/lib/board/provider/reply_notifier_provider.dart new file mode 100644 index 0000000000..eb683e703c --- /dev/null +++ b/frontend/lib/board/provider/reply_notifier_provider.dart @@ -0,0 +1,25 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class ReplyNotifier extends StateNotifier> { + ReplyNotifier(this.ref) : super([-1, -1, -1]); + + final Ref ref; + + Future add(int type, int index) async { + if (type == 0) { + // add comment + state = [index, -1, -1]; + } else if (type == 1) { + // modify comment + state = [-1, index, -1]; + } else { + // delete comment + state = [-1, -1, index]; + } + } +} + +final replyStateProvider = + StateNotifierProvider>((ref) { + return ReplyNotifier(ref); +}); diff --git a/frontend/lib/board/provider/reply_provider.dart b/frontend/lib/board/provider/reply_provider.dart new file mode 100644 index 0000000000..e3597e04c1 --- /dev/null +++ b/frontend/lib/board/provider/reply_provider.dart @@ -0,0 +1,51 @@ +import 'package:dio/dio.dart' hide Headers; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/common/const/data.dart'; +import 'package:frontend/common/provider/dio_provider.dart'; +import 'package:retrofit/http.dart'; + +part 'reply_provider.g.dart'; + +final replyProvider = Provider((ref) { + final dio = ref.watch(dioProvider); + + return ReplyNotifier(dio, baseUrl: ip); +}); + +@RestApi() +abstract class ReplyNotifier { + factory ReplyNotifier(Dio dio, {String baseUrl}) = _ReplyNotifier; + + @POST('/api/replies') + @Headers({ + 'accessToken': 'true', + }) + Future post( + @Body() Map data, + ); + + @PUT('/api/replies/{replyId}') + @Headers({ + 'accessToken': 'true', + }) + Future modify( + @Path() int replyId, + @Body() Map data, + ); + + @DELETE('/api/replies/{replyId}') + @Headers({ + 'accessToken': 'true', + }) + Future delete( + @Path() int replyId, + ); + + @POST('/api/replies/likes') + @Headers({ + 'accessToken': 'true', + }) + Future heart( + @Body() Map data, + ); +} diff --git a/frontend/lib/board/provider/reply_provider.g.dart b/frontend/lib/board/provider/reply_provider.g.dart new file mode 100644 index 0000000000..4cfa8ac263 --- /dev/null +++ b/frontend/lib/board/provider/reply_provider.g.dart @@ -0,0 +1,156 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'reply_provider.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers + +class _ReplyNotifier implements ReplyNotifier { + _ReplyNotifier( + this._dio, { + this.baseUrl, + }); + + final Dio _dio; + + String? baseUrl; + + @override + Future post(Map data) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final _data = {}; + _data.addAll(data); + await _dio.fetch(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/replies', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + } + + @override + Future modify( + int replyId, + Map data, + ) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final _data = {}; + _data.addAll(data); + await _dio.fetch(_setStreamType(Options( + method: 'PUT', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/replies/${replyId}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + } + + @override + Future delete(int replyId) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final Map? _data = null; + await _dio.fetch(_setStreamType(Options( + method: 'DELETE', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/replies/${replyId}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + } + + @override + Future heart(Map data) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final _data = {}; + _data.addAll(data); + await _dio.fetch(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/replies/likes', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/frontend/lib/board/provider/report_provider.dart b/frontend/lib/board/provider/report_provider.dart new file mode 100644 index 0000000000..3f7c689f4a --- /dev/null +++ b/frontend/lib/board/provider/report_provider.dart @@ -0,0 +1,27 @@ +import 'package:dio/dio.dart' hide Headers; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/common/provider/dio_provider.dart'; +import 'package:retrofit/http.dart'; + +import '../../common/const/data.dart'; + +part 'report_provider.g.dart'; + +final reportProvider = Provider((ref) { + final dio = ref.watch(dioProvider); + + return Report(dio, baseUrl: ip); +}); + +@RestApi() +abstract class Report { + factory Report(Dio dio, {String baseUrl}) = _Report; + + @POST('/api/report') + @Headers({ + 'accessToken': 'true', + }) + Future post( + @Body() Map data, + ); +} diff --git a/frontend/lib/board/provider/report_provider.g.dart b/frontend/lib/board/provider/report_provider.g.dart new file mode 100644 index 0000000000..dbcc1645cc --- /dev/null +++ b/frontend/lib/board/provider/report_provider.g.dart @@ -0,0 +1,76 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'report_provider.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers + +class _Report implements Report { + _Report( + this._dio, { + this.baseUrl, + }); + + final Dio _dio; + + String? baseUrl; + + @override + Future post(Map data) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final _data = {}; + _data.addAll(data); + await _dio.fetch(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/report', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/frontend/lib/board/provider/scrap_provider.dart b/frontend/lib/board/provider/scrap_provider.dart new file mode 100644 index 0000000000..34cf70a53b --- /dev/null +++ b/frontend/lib/board/provider/scrap_provider.dart @@ -0,0 +1,34 @@ +import 'package:dio/dio.dart' hide Headers; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/common/provider/dio_provider.dart'; +import 'package:retrofit/http.dart'; +import '../../common/const/data.dart'; + +part 'scrap_provider.g.dart'; + +final scrapProvider = Provider((ref) { + final dio = ref.watch(dioProvider); + + return Scrap(dio, baseUrl: ip); +}); + +@RestApi() +abstract class Scrap { + factory Scrap(Dio dio, {String baseUrl}) = _Scrap; + + @POST('/api/scrap') + @Headers({ + 'accessToken': 'true', + }) + Future post( + @Query('postId') int postId, + ); + + @DELETE('/api/scrap/{postId}') + @Headers({ + 'accessToken': 'true', + }) + Future delete( + @Path('postId') int postId, + ); +} diff --git a/frontend/lib/board/provider/scrap_provider.g.dart b/frontend/lib/board/provider/scrap_provider.g.dart new file mode 100644 index 0000000000..9a8f160ab1 --- /dev/null +++ b/frontend/lib/board/provider/scrap_provider.g.dart @@ -0,0 +1,100 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'scrap_provider.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers + +class _Scrap implements Scrap { + _Scrap( + this._dio, { + this.baseUrl, + }); + + final Dio _dio; + + String? baseUrl; + + @override + Future post(int postId) async { + const _extra = {}; + final queryParameters = {r'postId': postId}; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final Map? _data = null; + await _dio.fetch(_setStreamType(Options( + method: 'POST', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/scrap', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + } + + @override + Future delete(int postId) async { + const _extra = {}; + final queryParameters = {}; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final Map? _data = null; + await _dio.fetch(_setStreamType(Options( + method: 'DELETE', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/scrap/${postId}', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/frontend/lib/board/provider/search_repository_provider.dart b/frontend/lib/board/provider/search_repository_provider.dart new file mode 100644 index 0000000000..9050dd0811 --- /dev/null +++ b/frontend/lib/board/provider/search_repository_provider.dart @@ -0,0 +1,31 @@ +import 'package:dio/dio.dart' hide Headers; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/common/model/cursor_pagination_model.dart'; +import 'package:frontend/common/provider/dio_provider.dart'; +import 'package:retrofit/http.dart'; + +import '../../common/const/data.dart'; +import '../model/msg_board_response_model.dart'; + +part 'search_repository_provider.g.dart'; + +final searchRepositoryProvider = Provider((ref) { + final dio = ref.watch(dioProvider); + + return SearchRepository(dio, baseUrl: ip); +}); + +@RestApi() +abstract class SearchRepository { + factory SearchRepository(Dio dio, {String baseUrl}) = _SearchRepository; + + @GET('/api/post') + @Headers({ + 'accessToken': 'true', + }) + Future> paginate( + @Query('lastId') int lastId, + @Query('size') int size, + @Query('keyword') String keyword, + ); +} diff --git a/frontend/lib/board/provider/search_repository_provider.g.dart b/frontend/lib/board/provider/search_repository_provider.g.dart new file mode 100644 index 0000000000..faf16412c4 --- /dev/null +++ b/frontend/lib/board/provider/search_repository_provider.g.dart @@ -0,0 +1,89 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'search_repository_provider.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers + +class _SearchRepository implements SearchRepository { + _SearchRepository( + this._dio, { + this.baseUrl, + }); + + final Dio _dio; + + String? baseUrl; + + @override + Future> paginate( + int lastId, + int size, + String keyword, + ) async { + const _extra = {}; + final queryParameters = { + r'lastId': lastId, + r'size': size, + r'keyword': keyword, + }; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final Map? _data = null; + final _result = await _dio.fetch>( + _setStreamType>(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/post', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = CursorPaginationModel.fromJson( + _result.data!, + (json) => MsgBoardResponseModel.fromJson(json as Map), + ); + return value; + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/frontend/lib/board/provider/search_state_notifier_provider.dart b/frontend/lib/board/provider/search_state_notifier_provider.dart new file mode 100644 index 0000000000..2740b8b7c0 --- /dev/null +++ b/frontend/lib/board/provider/search_state_notifier_provider.dart @@ -0,0 +1,133 @@ +import 'dart:core'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/board/provider/search_repository_provider.dart'; +import 'package:frontend/board/provider/searck_keyword_provider.dart'; +import 'package:frontend/common/model/cursor_pagination_model.dart'; + +final searchStateNotifierProvider = + StateNotifierProvider( + (ref) { + final repository = ref.watch(searchRepositoryProvider); + const initialLastPostId = 9223372036854775807; + const size = 10; + final keyword = ref.watch(searchKeywordProvider.notifier).state; + + final notifier = SearchStateNotifier( + repository: repository, + lastId: initialLastPostId, + size: size, + keyword: keyword, + ); + return notifier; + }, +); + +class SearchStateNotifier extends StateNotifier { + bool _mounted = true; + bool _fetchingData = false; + + @override + void dispose() { + _mounted = false; + super.dispose(); + } + + final SearchRepository repository; + int lastId; + int size; + String keyword; + + SearchStateNotifier({ + required this.repository, + required this.lastId, + required this.size, + required this.keyword, + }) : super(CursorPaginationModelLoading()) { + paginate(); + } + + bool get isMounted => _mounted; + + void updateAndFetch(String newKeyword) { + keyword = newKeyword; + lastId = 9223372036854775807; + paginate(forceRefetch: true); + } + + void resetSearchResults() { + state = CursorPaginationModelLoading(); + } + + Future paginate({ + bool fetchMore = false, + bool forceRefetch = false, + }) async { + if (!isMounted) return; + + if (_fetchingData) return; + _fetchingData = true; + + try { + if (state is CursorPaginationModel && !forceRefetch) { + final pState = state as CursorPaginationModel; + + if (!pState.meta.hasMore) { + return; + } + } + + final isLoading = state is CursorPaginationModelLoading; + final isRefetching = state is CursorPaginationModelRefetching; + final isFetchingMore = state is CursorPaginationModelFetchingMore; + + if (fetchMore && (isLoading || isRefetching || isFetchingMore)) { + return; + } + + if (fetchMore) { + final pState = (state as CursorPaginationModel); + + state = CursorPaginationModelFetchingMore( + meta: pState.meta, + data: pState.data, + ); + lastId = pState.data.last.id; + } else { + if (state is CursorPaginationModel && !forceRefetch) { + final pState = state as CursorPaginationModel; + + state = CursorPaginationModelRefetching( + meta: pState.meta, + data: pState.data, + ); + } else { + state = CursorPaginationModelLoading(); + } + } + final resp = await repository.paginate(lastId, size, keyword); + + if (!isMounted) return; + + if (state is CursorPaginationModelFetchingMore) { + final pState = state as CursorPaginationModelFetchingMore; + state = resp.copyWith( + data: [ + ...pState.data, + ...resp.data, + ], + ); + } else { + state = resp; + } + } catch (e) { + if (!isMounted) return; + + print(e.runtimeType); + + state = CursorPaginationModelError(message: '데이터를 가져오지 못했습니다'); + } finally { + _fetchingData = false; + } + } +} diff --git a/frontend/lib/board/provider/searck_keyword_provider.dart b/frontend/lib/board/provider/searck_keyword_provider.dart new file mode 100644 index 0000000000..0513420492 --- /dev/null +++ b/frontend/lib/board/provider/searck_keyword_provider.dart @@ -0,0 +1,3 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final searchKeywordProvider = StateProvider((ref) => ''); \ No newline at end of file diff --git a/frontend/lib/board/view/msg_board_add_screen.dart b/frontend/lib/board/view/msg_board_add_screen.dart new file mode 100644 index 0000000000..0956afa536 --- /dev/null +++ b/frontend/lib/board/view/msg_board_add_screen.dart @@ -0,0 +1,884 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/board/const/categorys.dart'; +import 'package:frontend/board/layout/text_with_icon.dart'; +import 'package:frontend/board/model/exception_model.dart'; +import 'package:frontend/board/model/msg_board_response_model.dart'; +import 'package:frontend/board/provider/board_add_provider.dart'; +import 'package:frontend/board/provider/board_state_notifier_provider.dart'; +import 'package:frontend/board/provider/image_provider.dart'; +import 'package:frontend/board/provider/isquestion_provider.dart'; +import 'package:frontend/board/provider/network_image_provider.dart'; +import 'package:frontend/common/const/colors.dart'; +import 'package:frontend/member/provider/mypage/my_post_state_notifier_provider.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:crypto/crypto.dart'; + +import 'package:http/http.dart' as http; + +import '../../common/const/ip_list.dart'; + +class MsgBoardAddScreen extends ConsumerStatefulWidget { + final bool isEdit; + final MsgBoardResponseModel board; + const MsgBoardAddScreen( + {super.key, required this.isEdit, required this.board}); + + @override + ConsumerState createState() => _MsgBoardAddScreenState(); +} + +class _MsgBoardAddScreenState extends ConsumerState { + late BoardAdd boardAddAPI; + bool canUpload = false; + bool writedTitle = false; + bool writedContent = false; + String selectCategory = "자유게시판"; + String title = "", content = ""; + bool isQuestion = false; + List realImages = []; + List networkImages = []; + late TextEditingController titleController; + late TextEditingController contentController; + bool isLoading = false; + + @override + void initState() { + super.initState(); + if (widget.isEdit) { + selectCategory = + categoryCodesReverseList[widget.board.communityTitle].toString(); + title = widget.board.postTitle; + content = widget.board.postContent; + isQuestion = widget.board.isQuestion; + canUpload = widget.isEdit; + + canUpload = writedTitle = writedContent = true; + } + + titleController = TextEditingController(text: title); + contentController = TextEditingController(text: content); + } + + void refresh() async { + ref.read(boardStateNotifierProvider.notifier).lastId = 9223372036854775807; + await ref + .read(boardStateNotifierProvider.notifier) + .paginate(forceRefetch: true); + ref.read(myPostStateNotifierProvider.notifier).lastId = 9223372036854775807; + await ref + .read(myPostStateNotifierProvider.notifier) + .paginate(forceRefetch: true); + } + + void notAllowed(String s) { + showDialog( + context: context, + builder: ((context) { + return AlertDialog( + content: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + s, + overflow: TextOverflow.visible, + style: const TextStyle( + color: Colors.black, + fontSize: 13, + ), + ), + ), + ], + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: PRIMARY50_COLOR, + ), + child: const Text( + "확인", + style: TextStyle(fontSize: 13, color: PRIMARY_COLOR), + ), + ), + ], + ), + ], + ); + })); + } + + void filterDialog(String s) { + showDialog( + barrierDismissible: false, + context: context, + builder: ((context) { + return AlertDialog( + content: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + s, + overflow: TextOverflow.visible, + style: const TextStyle( + color: Colors.black, + fontSize: 13, + ), + ), + ), + ], + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + + upLoad(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: PRIMARY50_COLOR, + ), + child: const Text( + "네", + style: TextStyle(fontSize: 13, color: PRIMARY_COLOR), + ), + ), + const SizedBox( + width: 15, + ), + ElevatedButton( + onPressed: () { + setState(() { + isLoading = false; + }); + Navigator.of(context).pop(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: PRIMARY50_COLOR, + ), + child: const Text( + "아니요", + style: TextStyle(fontSize: 13, color: PRIMARY_COLOR), + ), + ), + ], + ), + ], + ); + })); + } + + String getRandomStr() { + String dt = DateTime.now().toString(); + final bytes = utf8.encode(dt); + final hash = sha256.convert(bytes); + return hash.toString(); + } + + void checkFilter() async { + var dio = Dio(); + try { + Response titleCheck = + await dio.post('$pythonIP/predict/', data: {"message": title}); + debugPrint("titleCheck : ${titleCheck.data["profanity"]}"); + + Response contentCheck = + await dio.post('$pythonIP/predict/', data: {"message": content}); + debugPrint("titleCheck : ${contentCheck.data["profanity"]}"); + if (titleCheck.data["profanity"] || contentCheck.data["profanity"]) { + filterDialog( + "제목이나 글 내용에 비속어가 포함되어 있는 경우 서비스 이용에 제한이 있을 수 있습니다. 정말 등록하시겠습니까?"); + } else { + upLoad(); + } + } catch (e) { + debugPrint("upload_predict : ${e.toString()}"); + } + } + + Future upLoad() async { + List images = []; + int i = 0; + for (; i < networkImages.length; i++) { + // network image URL + if (networkImages[i].contains("jpg")) { + images.add("${getRandomStr()}$i.jpg"); + } else if (networkImages[i].contains("png")) { + images.add("${getRandomStr()}$i.png"); + } else if (networkImages[i].contains("jpeg")) { + images.add("${getRandomStr()}$i.jpeg"); + } else if (networkImages[i].contains("gif")) { + images.add("${getRandomStr()}$i.gif"); + } else if (networkImages[i].contains("HEIC")) { + images.add("${getRandomStr()}$i.HEIC"); + } + } + for (; i - networkImages.length < realImages.length; i++) { + // local image path + if (realImages[i - networkImages.length].path.endsWith("jpg")) { + images.add("${getRandomStr()}$i.jpg"); + } else if (realImages[i - networkImages.length].path.endsWith("png")) { + images.add("${getRandomStr()}$i.png"); + } else if (realImages[i - networkImages.length].path.endsWith("jpeg")) { + images.add("${getRandomStr()}$i.jpeg"); + } else if (realImages[i - networkImages.length].path.endsWith("gif")) { + images.add("${getRandomStr()}$i.gif"); + } else if (realImages[i - networkImages.length].path.endsWith("HEIC")) { + images.add("${getRandomStr()}$i.HEIC"); + } + } + + if (images.length > 10) { + notAllowed("사진은 최대 10개까지만 가능합니다."); + setState(() { + isLoading = false; + }); + return; + } + + if (widget.isEdit) { + final requestData = { + 'communityTitle': categoryCodesList[selectCategory], + 'postId': widget.board.id, + 'title': title, + 'content': content, + 'images': images, + }; + List httpImages = []; + for (int i = 0; i < networkImages.length; i++) { + httpImages.add(await http.get(Uri.parse(networkImages[i]))); + } + MsgBoardResponseModel resp; + try { + resp = await boardAddAPI.modify(requestData); + } on DioException catch (e) { + if (e.response != null) { + Map data = e.response!.data; + ExceptionModel exc = ExceptionModel.fromJson(data); + debugPrint("boardModifyError : ${exc.message}"); + notAllowed(exc.message); + } else { + debugPrint("boardModifyError : ${e.message}"); + notAllowed(e.message!); + } + return; + } catch (e) { + debugPrint("boardModifyError : $e"); + notAllowed("다시 시도해주세요!"); + return; + } finally { + setState(() { + isLoading = false; + }); + } + + i = 0; + for (; i < resp.images.length; i++) { + final String url = resp.images[i]; + if (i < networkImages.length) { + UploadFile().httpFile(url, httpImages[i]); + } else { + if (i == resp.images.length - 1) { + // 마지막 사진만 업로드가 다 될때까지 기다림 + await UploadFile() + .file(url, File(realImages[i - networkImages.length].path)); + } else { + UploadFile() + .file(url, File(realImages[i - networkImages.length].path)); + } + } + } + } else { + final requestData = { + 'communityTitle': categoryCodesList[selectCategory], + 'title': title, + 'content': content, + 'isQuestion': isQuestion, + 'images': images, + }; + MsgBoardResponseModel resp; + try { + resp = await boardAddAPI.post(requestData); + } on DioException catch (e) { + if (e.response != null) { + Map data = e.response!.data; + ExceptionModel exc = ExceptionModel.fromJson(data); + debugPrint("boardPostError : ${exc.message}"); + notAllowed(exc.message); + return; + } else { + debugPrint("boardPostError : ${e.message}"); + notAllowed(e.message!); + return; + } + } catch (e) { + debugPrint("boardPostError : ${e.toString()}"); + notAllowed("다시 시도해주세요!"); + return; + } finally { + setState(() { + isLoading = false; + }); + } + + for (int i = 0; i < resp.images.length; i++) { + final String url = resp.images[i]; + if (i == resp.images.length - 1) { + await UploadFile().file(url, File(realImages[i].path)); + } else { + UploadFile().file(url, File(realImages[i].path)); + } + } + } + + refresh(); + + ref.read(imageStateProvider.notifier).clear(); + ref.read(networkImageStateProvider.notifier).clear(); + + Navigator.of(context).pop(); + } + + void upLoadDialog() { + if (canUpload) { + showDialog( + context: context, + builder: ((context) { + return AlertDialog( + content: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + widget.isEdit + ? Text("'$selectCategory'에 글을 수정할까요?") + : Text("'$selectCategory'에 글을 등록할까요?"), + ], + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + setState(() { + isLoading = true; + }); + checkFilter(); + }, + child: const Text("네"), + ), + const SizedBox( + width: 20, + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("아니요"), + ), + ], + ), + ], + ); + })); + } + } + + @override + Widget build(BuildContext context) { + return Stack(children: [ + Scaffold( + backgroundColor: Colors.white, + resizeToAvoidBottomInset: false, + appBar: AppBar( + backgroundColor: PRIMARY10_COLOR, + shadowColor: Colors.black, + elevation: 3, + iconTheme: const IconThemeData( + color: Colors.black, + ), + title: Text( + widget.isEdit ? "글 수정" : "글 작성", + style: const TextStyle( + fontSize: 15, + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + actions: [ + TextButton( + onPressed: upLoadDialog, + child: Text( + "완료", + style: TextStyle( + color: canUpload ? Colors.black : Colors.grey, + ), + ), + ), + ], + ), + body: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 15, + ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: TextField( + controller: titleController, + onChanged: (value) { + setState(() { + if (value != "") { + title = value; // 제목 + writedTitle = true; + } else { + writedTitle = false; + } + canUpload = writedTitle & writedContent; + }); + }, + decoration: InputDecoration( + border: InputBorder.none, + hintText: "제목", + disabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: + BODY_TEXT_COLOR.withOpacity(0.5)))), + ), + ), + SizedBox( + height: 25, + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(50), + ), + child: Padding( + padding: const EdgeInsets.only(left: 15, right: 10), + child: DropdownButton( + value: selectCategory, + icon: const Icon(Icons.arrow_drop_down_outlined), + style: const TextStyle( + color: Colors.black, + fontSize: 10, + ), + underline: Container(), + elevation: 0, + dropdownColor: Colors.grey.shade200, + borderRadius: BorderRadius.circular(20), + items: categorysList + .sublist(2, categorysList.length) + .map>( + (String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + onChanged: (value) => { + setState(() { + if (value != null) { + selectCategory = value; // 게시판 종류 + } + }) + }, + ), + ), + ), + ), + ], + ), + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: BODY_TEXT_COLOR.withOpacity(0.3), + width: 1, + ), + ), + ), + ), + const SizedBox( + height: 10, + ), + Container( + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50), + color: PRIMARY20_COLOR, + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "잠깐!", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 10, + ), + ), + SizedBox( + width: 5, + ), + Flexible( + child: Text( + "부적절하거나 불쾌감을 줄 수 있는 컨텐츠는 제재를 받을 수 있습니다.", + overflow: TextOverflow.visible, + style: TextStyle( + fontSize: 10, + ), + ), + ), + ], + ), + ), + TextField( + controller: contentController, + onChanged: (value) { + setState(() { + if (value != "") { + content = value; // 내용 + writedContent = true; + } else { + writedContent = false; + } + canUpload = writedTitle & writedContent; + }); + }, + keyboardType: TextInputType.multiline, + maxLines: 20, + style: const TextStyle( + fontSize: 12, + ), + decoration: const InputDecoration( + hintText: "지금 가장 고민이 되거나 궁금한 내용이 무엇인가요?", + border: InputBorder.none, + hintStyle: TextStyle( + fontSize: 12, + ), + ), + ), + ], + ), + ), + BottomView( + widget: widget, + msgBoardAddScreenState: this, + ), + ], + ), + ), + Transform.translate( + offset: const Offset(80.0, 570.0), + child: Image.asset( + 'asset/imgs/logo.png', + width: 450.0, + opacity: const AlwaysStoppedAnimation(.2), + ), + ), + isLoading + ? Container( + color: Colors.black.withOpacity(0.4), + child: const Center( + child: CircularProgressIndicator(), + ), + ) + : const Center(), + ]); + } +} + +class BottomView extends ConsumerWidget { + const BottomView( + {super.key, required this.widget, required this.msgBoardAddScreenState}); + + final MsgBoardAddScreen widget; + final _MsgBoardAddScreenState msgBoardAddScreenState; + + @override + Widget build(BuildContext context, WidgetRef ref) { + msgBoardAddScreenState.boardAddAPI = ref.watch(boardAddProvider); + if (!msgBoardAddScreenState.widget.isEdit) { + msgBoardAddScreenState.isQuestion = ref.watch(isQuestionStateProvider); + } + return Column( + children: [ + ImageViewer( + msgBoardAddScreenState: msgBoardAddScreenState, + ), + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: BODY_TEXT_COLOR.withOpacity(0.3), + width: 1, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 10, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Row( + children: [ + TextWithIcon( + icon: Icons.image_rounded, + iconSize: 17, + text: "사진", + commentId: -1, + postId: -1, + replyId: -1, + isClicked: false, + isMine: false, + userId: -1, + ), + ], + ), + if (widget.isEdit) + TextButton( + onPressed: () async { + await ref.watch(boardAddProvider).delete(widget.board.id); + ref.read(boardStateNotifierProvider.notifier).lastId = + 9223372036854775807; + await ref + .read(boardStateNotifierProvider.notifier) + .paginate(forceRefetch: true); + Navigator.of(context).pop(); + Navigator.of(context).pop(); + }, + child: const Text( + "삭제", + style: TextStyle( + color: Colors.red, + ), + ), + ), + ], + ), + ), + const SizedBox( + height: 80, + ), + ], + ); + } +} + +class ImageViewer extends ConsumerWidget { + const ImageViewer({super.key, required this.msgBoardAddScreenState}); + final _MsgBoardAddScreenState msgBoardAddScreenState; + @override + Widget build(BuildContext context, WidgetRef ref) { + List images; + images = ref.watch(imageStateProvider); + msgBoardAddScreenState.realImages = images; + if (msgBoardAddScreenState.widget.isEdit) { + msgBoardAddScreenState.networkImages = + msgBoardAddScreenState.widget.board.images; + for (var removeImg in ref.watch(networkImageStateProvider)) { + msgBoardAddScreenState.networkImages.remove(removeImg); + } + try { + return Padding( + padding: const EdgeInsets.all(10), + child: SizedBox( + height: 100, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + for (var image in msgBoardAddScreenState.networkImages) + Stack( + children: [ + Container( + margin: const EdgeInsets.only( + right: 10, + left: 10, + top: 10, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.black.withOpacity(0.2), + ), + width: 100, + child: Image( + image: NetworkImage(image), + ), + ), + const Padding( + padding: EdgeInsets.all(12), + child: Icon( + Icons.cancel_outlined, + color: Colors.white, + ), + ), + IconButton( + onPressed: () { + ref + .read(networkImageStateProvider.notifier) + .add(image); + }, + icon: const Icon( + Icons.cancel, + color: Colors.red, + ), + ), + ], + ), + for (var image in images) + Stack( + children: [ + Container( + margin: const EdgeInsets.only( + right: 10, + left: 10, + top: 10, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.black.withOpacity(0.2), + ), + width: 100, + child: Image.file( + File(image.path), + ), + ), + const Padding( + padding: EdgeInsets.all(12), + child: Icon( + Icons.cancel_outlined, + color: Colors.white, + ), + ), + IconButton( + onPressed: () { + ref.read(imageStateProvider.notifier).remove(image); + }, + icon: const Icon( + Icons.cancel, + color: Colors.red, + ), + ), + ], + ), + ], + ), + ), + ); + } catch (e) { + debugPrint( + "ImageViewer error! ${msgBoardAddScreenState.networkImages[0]}"); + return const SizedBox( + height: 100, + width: 100, + ); + } + } else { + try { + return Padding( + padding: const EdgeInsets.all(10), + child: SizedBox( + height: 100, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + for (var image in images) + Stack( + children: [ + Container( + margin: const EdgeInsets.only( + right: 10, + left: 10, + top: 10, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.black.withOpacity(0.2), + ), + width: 100, + child: Image.file( + File(image.path), + ), + ), + const Padding( + padding: EdgeInsets.all(12), + child: Icon( + Icons.cancel_outlined, + color: Colors.white, + ), + ), + IconButton( + onPressed: () { + ref.read(imageStateProvider.notifier).remove(image); + }, + icon: const Icon( + Icons.cancel, + color: Colors.red, + ), + ), + ], + ), + ], + ), + ), + ); + } catch (e) { + debugPrint("ImageViewer error! ${images[0].path}"); + return const SizedBox( + height: 100, + width: 100, + ); + } + } + } +} + +class UploadFile { + Future file(String url, File image) async { + try { + var response = + await http.put(Uri.parse(url), body: image.readAsBytesSync()); + if (response.statusCode != 200) { + debugPrint("upload file 응답 : ${response.statusCode}"); + } + } catch (e) { + debugPrint("upload file 에러 : $e"); + } + } + + Future httpFile(String url, http.Response image) async { + try { + var response = await http.put(Uri.parse(url), body: image.bodyBytes); + if (response.statusCode != 200) { + debugPrint("Upload HTTP File 응답 : ${response.statusCode}"); + } + } catch (e) { + debugPrint("Upload HTTP File 에러 : $e"); + } + } +} diff --git a/frontend/lib/board/view/msg_board_list_screen.dart b/frontend/lib/board/view/msg_board_list_screen.dart new file mode 100644 index 0000000000..4f1530adc3 --- /dev/null +++ b/frontend/lib/board/view/msg_board_list_screen.dart @@ -0,0 +1,425 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:frontend/board/component/board_card.dart'; +import 'package:frontend/board/model/msg_board_detail_response_model.dart'; +import 'package:frontend/board/provider/board_add_provider.dart'; +import 'package:frontend/board/provider/board_detail_state_notifier_provider.dart'; +import 'package:frontend/board/provider/board_state_notifier_provider.dart'; +import 'package:frontend/board/const/categorys.dart'; +import 'package:frontend/board/provider/comment_pagination_provider.dart'; +import 'package:frontend/board/provider/payload_state_notifier_provider.dart'; +import 'package:frontend/board/view/msg_board_add_screen.dart'; +import 'package:frontend/board/view/msg_board_screen.dart'; +import 'package:frontend/board/view/search_screen.dart'; +import 'package:frontend/common/const/colors.dart'; +import 'package:frontend/board/model/msg_board_response_model.dart'; +import 'package:frontend/common/model/cursor_pagination_model.dart'; +import 'package:frontend/common/provider/dio_provider.dart'; +import 'package:frontend/member/provider/mypage/my_comment_state_notifier_provider.dart'; + +import '../../common/component/notice_popup_dialog.dart'; +import '../../common/const/data.dart'; +import '../../member/model/member_model.dart'; +import '../../member/provider/member_state_notifier_provider.dart'; +import '../../member/view/my_page_screen.dart'; +import '../component/category_circle_with_provider.dart'; + +class MsgBoardListScreen extends ConsumerStatefulWidget { + static String get routeName => 'boardList'; + + const MsgBoardListScreen({ + super.key, + }); + + @override + ConsumerState createState() => _MsgBoardListScreenState(); +} + +class _MsgBoardListScreenState extends ConsumerState { + List categorys = categorysList; + final ScrollController controller = ScrollController(); + String payload = ""; + + @override + void initState() { + super.initState(); + controller.addListener(scrollListener); + } + + void scrollListener() { + if (controller.offset > controller.position.maxScrollExtent - 150) { + ref.read(boardStateNotifierProvider.notifier).paginate(fetchMore: true); + } + } + + @override + Widget build(BuildContext context) { + payload = ref.watch(payloadNotifier); + if (payload != "") { + debugPrint("Show Payload Page : $payload"); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + await ref.read(payloadNotifier.notifier).add(""); + ref.read(boardDetailNotifier.notifier).add(int.parse(payload)); + MsgBoardDetailResponseModel resp; + ref + .read(commentPaginationProvider.notifier) + .paginate(forceRefetch: true); + ref.read(myCommentStateNotifierProvider.notifier).lastId = + 9223372036854775807; + ref + .read(myCommentStateNotifierProvider.notifier) + .paginate(forceRefetch: true); + ref.watch(boardAddProvider).get(int.parse(payload)).then((value) { + resp = value; + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MsgBoardScreen( + board: resp, + ), + fullscreenDialog: true), + ); + }); + }); + } + + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Column( + children: [ + renderTop(), + renderCategories(), + Expanded( + child: RefreshIndicator( + onRefresh: () async { + ref.read(boardStateNotifierProvider.notifier).lastId = + 9223372036854775807; + await ref + .read(boardStateNotifierProvider.notifier) + .paginate(forceRefetch: true); + }, + child: ListView( + controller: controller, + physics: const AlwaysScrollableScrollPhysics(), + children: [ + renderBoardList(), + ], + ), + ), + ), + ], + ), + ), + floatingActionButton: Stack( + fit: StackFit.expand, + children: [ + Positioned( + right: 10, + bottom: 100, + child: FloatingActionButton( + heroTag: 'searchButton', + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const SearchScreen(), + ), + ); + }, + shape: const CircleBorder(), + backgroundColor: Colors.blue[300], + child: const Icon( + Icons.search, + color: Colors.white, + ), + ), + ), + Positioned( + right: 10, + bottom: 30, + child: FloatingActionButton( + heroTag: 'addButton', + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => MsgBoardAddScreen( + isEdit: false, + board: MsgBoardResponseModel( + id: 0, + userId: 0, + userNickname: "", + universityName: "", + communityId: 0, + communityTitle: "", + postTitle: "", + postContent: "", + images: [], + count: ReactCountModel( + commentReplyCount: 0, likeCount: 0, scrapCount: 0), + isQuestion: false, + isBlockedUser: false, + createdDateTime: "", + imageCount: 0, + ), + ), + ), + ); + }, + shape: const CircleBorder(), + backgroundColor: PRIMARY50_COLOR, + child: const Icon( + Icons.add, + color: Colors.white, + ), + ), + ), + ], + ), + ); + } + + Widget renderMajorSelectBox() { + final memberState = ref.watch(memberStateNotifierProvider); + + String major = ""; + String minor = ""; + String activatedMajor = ""; + + if (memberState is MemberModel) { + major = memberState.major; + minor = memberState.minor; + activatedMajor = memberState.activatedDepartment; + } + + List majors = []; + if (activatedMajor == major) { + majors.add(major); + if (minor.isNotEmpty) majors.add(minor); + } else { + majors = [minor, major]; + } + + final dio = ref.watch(dioProvider); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: activatedMajor, + onChanged: (String? newValue) async { + if (newValue != null && activatedMajor != newValue) { + try { + // 활성화된 전공을 변경하는 API 요청을 보낸다. + final resp = await dio.put( + '$ip/api/belongs/switch-departments', + options: Options( + headers: { + 'accessToken': 'true', + }, + ), + ); + if (resp.statusCode == 200) { + // 다시 paginate api 요청을 보낸다. + ref.read(memberStateNotifierProvider.notifier).getMe(); + ref.read(boardStateNotifierProvider.notifier).lastId = + 9223372036854775807; + ref + .read(boardStateNotifierProvider.notifier) + .paginate(forceRefetch: true); + } + } catch (e) { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: "오류발생", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); + }, + ); + }, + ); + } + } + }, + icon: const Icon(Icons.keyboard_arrow_down_rounded), + style: const TextStyle( + color: Colors.black, + fontSize: 16, + ), + items: majors.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text( + value, + overflow: TextOverflow.fade, + ), + ); + }).toList(), + ), + ), + ); + } + + Widget renderTop() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 2.0), + height: 50.0, + width: MediaQuery.of(context).size.width, + child: Stack( + children: [ + Center( + child: GestureDetector( + onTap: () { + ref.read(boardStateNotifierProvider.notifier).lastId = + 9223372036854775807; + ref + .read(boardStateNotifierProvider.notifier) + .paginate(forceRefetch: true); + }, + child: Image.asset( + 'asset/imgs/logo.png', + width: 60.0, + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + renderMajorSelectBox(), + SizedBox( + width: 70, + child: IconButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const MyPageScreen(), + ), + ); + }, + icon: const Icon( + Icons.person, + ), + ), + ), + ], + ), + ], + )); + } + + Widget renderCategories() { + return Padding( + padding: const EdgeInsets.only(top: 10.0, bottom: 10.0, left: 5), + child: SizedBox( + height: 40, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + for (var category in categorys) + Padding( + padding: const EdgeInsets.all(7.0), + child: CategoryCircleWithProvider( + category: category, + categoryCode: categoryCodesList[category]!, + type: true, + ), + ) + ], + ), + ), + ); + } + + Widget renderBoardList() { + final data = ref.watch(boardStateNotifierProvider); + + if (data is CursorPaginationModelLoading) { + return const Center( + child: CircularProgressIndicator( + color: PRIMARY_COLOR, + ), + ); + } + + if (data is CursorPaginationModelError) { + return const Center( + child: Text("데이터를 불러올 수 없습니다."), + ); + } + + final cp = data as CursorPaginationModel; + for (int i = cp.data.length - 1; i >= 0; i--) { + final MsgBoardResponseModel pItem = cp.data[i]; + if (pItem.isBlockedUser) { + cp.data.removeAt(i); + } + } + + if (cp.data.isEmpty) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: MediaQuery.of(context).size.height/3), + const Text( + "해당 게시판에 작성된 게시글이 없습니다.", + style: TextStyle(color: BODY_TEXT_COLOR, fontSize: 16.0), + ), + ], + ); + } + + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: cp.data.length + 1, + itemBuilder: (_, index) { + if (index == cp.data.length) { + return Center( + child: cp is CursorPaginationModelFetchingMore + ? const CircularProgressIndicator( + color: PRIMARY_COLOR, + ) + : const Text( + 'Copyright 2024. Decl Team all rights reserved.\n', + style: TextStyle( + color: BODY_TEXT_COLOR, + fontSize: 12.0, + ), + ), + ); + } + + final MsgBoardResponseModel pItem = cp.data[index]; + + return GestureDetector( + child: BoardCard.fromModel(msgBoardResponseModel: pItem), + onTap: () async { + // 상세페이지 + ref.read(boardDetailNotifier.notifier).add(pItem.id); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MsgBoardScreen( + board: pItem, + ), + fullscreenDialog: true), + ); + }, + ); + }, + separatorBuilder: (_, index) { + return const SizedBox(height: 1.0); + }, + ); + } +} diff --git a/frontend/lib/board/view/msg_board_screen.dart b/frontend/lib/board/view/msg_board_screen.dart new file mode 100644 index 0000000000..95c3bf1e13 --- /dev/null +++ b/frontend/lib/board/view/msg_board_screen.dart @@ -0,0 +1,1029 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/board/const/categorys.dart'; +import 'package:frontend/board/model/comment_model.dart'; +import 'package:frontend/board/model/exception_model.dart'; +import 'package:frontend/board/model/msg_board_detail_response_model.dart'; +import 'package:frontend/board/model/msg_board_response_model.dart'; +import 'package:frontend/board/provider/block_provider.dart'; +import 'package:frontend/board/provider/board_add_provider.dart'; +import 'package:frontend/board/provider/board_state_notifier_provider.dart'; +import 'package:frontend/board/provider/comment_pagination_provider.dart'; +import 'package:frontend/board/provider/comment_provider.dart'; +import 'package:frontend/board/provider/comment_notifier_provider.dart'; +import 'package:frontend/board/provider/image_provider.dart'; +import 'package:frontend/board/provider/network_image_provider.dart'; +import 'package:frontend/board/provider/reply_notifier_provider.dart'; +import 'package:frontend/board/provider/reply_provider.dart'; +import 'package:frontend/board/provider/report_provider.dart'; +import 'package:frontend/board/view/msg_board_add_screen.dart'; +import 'package:frontend/common/const/colors.dart'; +import 'package:frontend/board/layout/board_layout.dart'; +import 'package:frontend/board/layout/comment_layout.dart'; +import 'package:frontend/common/model/cursor_pagination_model.dart'; +import 'package:frontend/member/model/member_model.dart'; +import 'package:frontend/member/provider/member_state_notifier_provider.dart'; +import 'package:frontend/member/provider/mypage/my_comment_state_notifier_provider.dart'; +import 'package:frontend/member/provider/mypage/my_post_state_notifier_provider.dart'; + +import '../../common/const/ip_list.dart'; + +class MsgBoardScreen extends ConsumerStatefulWidget { + final MsgBoardResponseModel board; + const MsgBoardScreen({super.key, required this.board}); + + @override + ConsumerState createState() => _MsgBoardScreenState(); +} + +class _MsgBoardScreenState extends ConsumerState { + @override + void initState() { + super.initState(); + controller.addListener(scrollListener); + } + + final TextEditingController textEditingController = TextEditingController(); + final ScrollController controller = ScrollController(); + final FocusNode _focusNode = FocusNode(); + bool firstTime = true; + double controllerOffset = 0; + + void moveScroll() { + controller.animateTo(controller.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), curve: Curves.ease); + } + + void scrollListener() { + if (controller.offset > controller.position.maxScrollExtent - 150) { + ref.read(commentPaginationProvider.notifier).paginate( + fetchMore: true, + ); + } + } + + void refresh() async { + controllerOffset = controller.offset; + + await ref + .read(commentPaginationProvider.notifier) + .paginate(forceRefetch: true); + ref.read(myCommentStateNotifierProvider.notifier).lastId = + 9223372036854775807; + ref + .read(myCommentStateNotifierProvider.notifier) + .paginate(forceRefetch: true); + + firstTime = true; + + while (controller.position.maxScrollExtent < controllerOffset) { + await controller.animateTo(controllerOffset + 30, + duration: const Duration(milliseconds: 300), curve: Curves.ease); + } + } + + void addNewComment() async { + final requestData = { + 'postId': widget.board.id.toInt(), + 'content': textEditingController.text, + }; + try { + await ref.watch(commentProvider).post(requestData); + refresh(); + moveScroll(); + } on DioException catch (e) { + if (e.response != null) { + Map data = e.response!.data; + ExceptionModel exc = ExceptionModel.fromJson(data); + notAllowed(exc.message); + } + } + } + + void addNewReply(int commentId) async { + final requestData = { + 'commentId': commentId, + 'content': textEditingController.text, + }; + + try { + await ref.watch(replyProvider).post(requestData); + } on DioException catch (e) { + if (e.response != null) { + Map data = e.response!.data; + ExceptionModel exc = ExceptionModel.fromJson(data); + notAllowed(exc.message); + } + } + + await ref.read(commentStateProvider.notifier).add(0, -1); + refresh(); + } + + void modifyComment(int commentId) async { + final requestData = { + 'content': textEditingController.text, + }; + await ref.watch(commentProvider).modify(commentId, requestData); + await ref.read(commentStateProvider.notifier).add(1, -1); + refresh(); + } + + void modifyReply(int replyId) async { + final requestData = { + 'content': textEditingController.text, + }; + await ref.watch(replyProvider).modify(replyId, requestData); + await ref.read(replyStateProvider.notifier).add(1, -1); + refresh(); + } + + void deleteComment(int commentId) async { + await ref.watch(commentProvider).delete(commentId); + ref.read(commentStateProvider.notifier).add(2, -1); + refresh(); + } + + void deleteReply(int replyId) async { + await ref.watch(replyProvider).delete(replyId); + ref.read(replyStateProvider.notifier).add(2, -1); + refresh(); + } + + void sendReport(String reason) async { + final data = { + 'reportedObjectId': widget.board.id, + 'reportType': "POST", + 'reason': reason, + }; + await ref.read(reportProvider).post(data); + notAllowed("신고되었습니다.\n검토까지는 최대 24시간 소요됩니다."); + } + + void selectReportReason() { + showDialog( + context: context, + builder: ((context) { + return AlertDialog( + actionsPadding: EdgeInsets.zero, + backgroundColor: PRIMARY10_COLOR, + content: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + "신고 사유를 선택해주세요.\n신고 사유에 맞지 않는 신고일 경우,\n해당 신고는 처리되지 않습니다.\n누적 신고횟수가 10회 이상인 게시글은 삭제되고\n해당 글의 작성자는 일정 기간 글과 댓글을 작성할 수 없게 됩니다.", + overflow: TextOverflow.visible, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, + fontSize: 13, + fontWeight: FontWeight.bold), + ), + ), + ], + ), + actions: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () { + Navigator.of(context).pop(); + sendReport('INSULTING'); + }, + child: const Text( + "욕설/비하", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: BODY_TEXT_COLOR.withOpacity(0.3), + width: 1, + ), + ), + ), + ), + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () { + Navigator.of(context).pop(); + sendReport('COMMERCIAL'); + }, + child: const Text( + "상업적 광고 및 판매", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: BODY_TEXT_COLOR.withOpacity(0.3), + width: 1, + ), + ), + ), + ), + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () { + Navigator.of(context).pop(); + sendReport('INAPPROPRIATE'); + }, + child: const Text( + "게시판 성격에 부적절함", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: BODY_TEXT_COLOR.withOpacity(0.3), + width: 1, + ), + ), + ), + ), + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () { + Navigator.of(context).pop(); + sendReport('FRAUD'); + }, + child: const Text( + "유출/사칭/사기", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: BODY_TEXT_COLOR.withOpacity(0.3), + width: 1, + ), + ), + ), + ), + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () { + Navigator.of(context).pop(); + sendReport('SPAM'); + }, + child: const Text( + "낚시/놀람/도배", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: BODY_TEXT_COLOR.withOpacity(0.3), + width: 1, + ), + ), + ), + ), + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () { + Navigator.of(context).pop(); + sendReport('PORNOGRAPHIC'); + }, + child: const Text( + "음란물/불건전한 만남 및 대화", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + ], + ), + ], + ); + })); + } + + void boardMore(bool isMine) { + try { + if (isMine) { + showDialog( + context: context, + builder: ((context) { + return AlertDialog( + actionsPadding: EdgeInsets.zero, + backgroundColor: PRIMARY10_COLOR, + actions: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () async { + await ref + .watch(boardAddProvider) + .delete(widget.board.id); + await ref + .read(boardStateNotifierProvider.notifier) + .paginate(forceRefetch: true); + await ref + .read(myPostStateNotifierProvider.notifier) + .paginate(forceRefetch: true); + Navigator.of(context).pop(); + Navigator.of(context).pop(); + }, + child: const Text( + "삭제하기", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: BODY_TEXT_COLOR.withOpacity(0.3), + width: 1, + ), + ), + ), + ), + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () { + Navigator.of(context).pop(); + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => MsgBoardAddScreen( + isEdit: true, + board: widget.board, + ), + ), + ); + }, + child: const Text( + "수정하기", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + ], + ), + ], + ); + })); + } else { + showDialog( + context: context, + builder: ((context) { + return AlertDialog( + actionsPadding: EdgeInsets.zero, + backgroundColor: PRIMARY10_COLOR, + actions: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () { + // TODO : Report Board + Navigator.of(context).pop(); + selectReportReason(); + }, + child: const Text( + "신고하기", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: BODY_TEXT_COLOR.withOpacity(0.3), + width: 1, + ), + ), + ), + ), + Row( + children: [ + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + backgroundColor: Colors.transparent, + ), + onPressed: () { + Navigator.of(context).pop(); + isReally(); + }, + child: const Text( + "차단하기", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + ), + ), + ], + ), + ], + ), + ], + ); + })); + } + } catch (e) { + debugPrint("myModel 불러오는 문제 발생 : $e"); + } + } + + void notification() { + // TODO : notification on/off + } + + void isReally() { + showDialog( + context: context, + builder: ((context) { + return AlertDialog( + content: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + "이 작성자의 게시물이 목록에 노출되지 않으며, 다시 해제할 수 없습니다.", + overflow: TextOverflow.visible, + style: TextStyle( + color: Colors.black, + fontSize: 13, + ), + ), + ), + ], + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + ref.read(blockProvider).post(widget.board.userId); + Navigator.of(context).pop(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: PRIMARY50_COLOR, + ), + child: const Text( + "확인", + style: TextStyle(fontSize: 13, color: PRIMARY_COLOR), + ), + ), + const SizedBox( + width: 10, + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: PRIMARY50_COLOR, + ), + child: const Text( + "취소", + style: TextStyle(fontSize: 13, color: PRIMARY_COLOR), + ), + ), + ], + ), + ], + ); + })); + } + + void notAllowed(String s) { + showDialog( + context: context, + builder: ((context) { + return AlertDialog( + content: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + s, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.black, + fontSize: 13, + ), + ), + ), + ], + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: PRIMARY50_COLOR, + ), + child: const Text( + "확인", + style: TextStyle(fontSize: 13, color: PRIMARY_COLOR), + ), + ), + ], + ), + ], + ); + })); + } + + void filterDialog(String s, List selectCommentIndex, + List selectReplyIndex) async { + await showDialog( + barrierDismissible: false, + context: context, + builder: ((context) { + return AlertDialog( + content: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + s, + overflow: TextOverflow.visible, + style: const TextStyle( + color: Colors.black, + fontSize: 13, + ), + ), + ), + ], + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + + if (selectCommentIndex[0] != -1) { + // Upload Reply + addNewReply(selectCommentIndex[0]); + } else if (selectCommentIndex[1] != -1) { + // Modify Comment + modifyComment(selectCommentIndex[1]); + } else if (selectReplyIndex[1] != -1) { + // Modify Reply + modifyReply(selectReplyIndex[1]); + } else { + addNewComment(); + } + + textEditingController.clear(); + FocusManager.instance.primaryFocus?.unfocus(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: PRIMARY50_COLOR, + ), + child: const Text( + "네", + style: TextStyle(fontSize: 13, color: PRIMARY_COLOR), + ), + ), + const SizedBox( + width: 15, + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: PRIMARY50_COLOR, + ), + child: const Text( + "아니요", + style: TextStyle(fontSize: 13, color: PRIMARY_COLOR), + ), + ), + ], + ), + ], + ); + })); + } + + @override + Widget build(BuildContext context) { + final memberState = ref.watch(memberStateNotifierProvider); + int myId = -1; + String myEmail = ""; + if (memberState is MemberModel) { + myId = memberState.id; + myEmail = memberState.email; + } + ref.watch(imageStateProvider); + List selectCommentIndex = ref.watch(commentStateProvider); + List selectReplyIndex = ref.watch(replyStateProvider); + if (selectCommentIndex[2] != -1) { + // Delete comment. + deleteComment(selectCommentIndex[2]); + } else if (selectReplyIndex[2] != -1) { + // Delete Reply + deleteReply(selectReplyIndex[2]); + } + if ((selectCommentIndex[0] != -1 || + selectCommentIndex[1] != -1 || + selectReplyIndex[1] != -1) && + firstTime) { + // AddReply, ModifyComment, ModifyReply -> show keyboard + FocusScope.of(context).requestFocus(_focusNode); + firstTime = false; + } + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + centerTitle: true, + backgroundColor: PRIMARY10_COLOR, + iconTheme: const IconThemeData( + color: Colors.black, + ), + shadowColor: Colors.black, + elevation: 3, + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + ref.read(commentStateProvider.notifier).add(0, -1); + ref.read(commentStateProvider.notifier).add(1, -1); + ref.read(replyStateProvider.notifier).add(1, -1); + Navigator.pop(context); + }, + ), + title: Text( + categoryCodesReverseList[widget.board.communityTitle].toString(), + style: const TextStyle( + fontSize: 15, + color: Colors.black, + fontWeight: FontWeight.normal, + ), + ), + actions: [ + IconButton( + onPressed: notification, + icon: myId == widget.board.userId + ? const Icon(Icons.notifications_none) + : const Icon(Icons.notifications_off_outlined), + ), + IconButton( + onPressed: () { + boardMore(myId == widget.board.userId); + }, + icon: const Icon(Icons.more_horiz), + ), + ], + ), + body: Column( + children: [ + Expanded( + child: RefreshIndicator( + onRefresh: () async { + ref + .read(commentPaginationProvider.notifier) + .paginate(forceRefetch: true); + ref.read(myCommentStateNotifierProvider.notifier).lastId = + 9223372036854775807; + ref + .read(myCommentStateNotifierProvider.notifier) + .paginate(forceRefetch: true); + }, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + controller: controller, + keyboardDismissBehavior: + ScrollViewKeyboardDismissBehavior.onDrag, + child: Column( + children: [ + renderBoardDetail(myId == widget.board.userId), + RenderCommentList( + ref: ref, + controller: controller, + selectCommentIndex: selectCommentIndex, + selectReplyIndex: selectReplyIndex, + myEmail: myEmail, + myId: myId), + ], + ), + ), + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + renderTextField(selectCommentIndex, selectReplyIndex), + KeyboardVisibilityBuilder( + builder: (p0, isKeyboardVisible) { + return isKeyboardVisible + ? const SizedBox( + height: 0, + ) + : Container( + height: 40, + decoration: + const BoxDecoration(color: Colors.white), + ); + }, + ), + ], + ), + ], + )); + } + + Widget renderBoardDetail(bool isMine) { + ref.watch(networkImageStateProvider); + return FutureBuilder( + future: ref.watch(boardAddProvider).get(widget.board.id), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const CircularProgressIndicator(); + } else if (snapshot.hasError) { + return const Text('이미 삭제된 글입니다.'); + } else { + MsgBoardDetailResponseModel boardDetail = + snapshot.data ?? widget.board as MsgBoardDetailResponseModel; + return Board( + board: boardDetail, + titleSize: 14, + isMine: isMine, + ); + } + }, + ); + } + + Widget renderTextField(selectCommentIndex, selectReplyIndex) { + return Container( + color: Colors.white, + child: Row( + children: [ + Expanded( + child: TextField( + focusNode: _focusNode, + controller: textEditingController, + decoration: InputDecoration( + hintText: '입력', + contentPadding: + const EdgeInsets.symmetric(vertical: 10, horizontal: 20), + border: OutlineInputBorder( + borderSide: BorderSide( + color: BODY_TEXT_COLOR.withOpacity(0.5), + ), + ), + suffixIcon: GestureDetector( + child: const Icon( + Icons.send, + color: PRIMARY50_COLOR, + size: 30, + ), + onTap: () async { + if (textEditingController.text == "") { + return; + } + try { + var dio = Dio(); + Response contentCheck = await dio.post( + '$pythonIP/predict/', + data: {"message": textEditingController.text}); + debugPrint( + "titleCheck : ${contentCheck.data["profanity"]}"); + if (contentCheck.data["profanity"]) { + filterDialog( + "댓글에 비속어가 포함되어 있는 경우 서비스 이용에 제한이 있을 수 있습니다. 정말 등록하시겠습니까?", + selectCommentIndex, + selectReplyIndex); + } else { + if (selectCommentIndex[0] != -1) { + // Upload Reply + addNewReply(selectCommentIndex[0]); + } else if (selectCommentIndex[1] != -1) { + // Modify Comment속 + modifyComment(selectCommentIndex[1]); + } else if (selectReplyIndex[1] != -1) { + // Modify Reply + modifyReply(selectReplyIndex[1]); + } else { + addNewComment(); + } + + textEditingController.clear(); + FocusManager.instance.primaryFocus?.unfocus(); + } + } catch (e) { + debugPrint("upload_content_predict : ${e.toString()}"); + } + }, + ), + ), + ), + ), + ], + ), + ); + } +} + +class RenderCommentList extends StatelessWidget { + const RenderCommentList({ + super.key, + required this.ref, + required this.controller, + required this.selectCommentIndex, + required this.selectReplyIndex, + required this.myEmail, + required this.myId, + }); + + final WidgetRef ref; + final ScrollController controller; + final List selectCommentIndex; + final List selectReplyIndex; + final String myEmail; + final int myId; + + @override + Widget build(BuildContext context) { + final data = ref.watch(commentPaginationProvider); + + if (data is CursorPaginationModelLoading) { + return const Center( + child: CircularProgressIndicator( + color: PRIMARY_COLOR, + ), + ); + } + + if (data is CursorPaginationModelError) { + return const Center( + child: Text("데이터를 불러올 수 없습니다."), + ); + } + + final cp = data as CursorPaginationModel; + for (int i = cp.data.length - 1; i >= 0; i--) { + if (cp.data[i].isBlockedUser) { + cp.data.removeAt(i); + } + } + + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: cp.data.length, + itemBuilder: (_, index) { + final CommentModel comment = cp.data[index]; + // debugPrint("$index 번째 댓글 : ${comment.content}"); + return Comment( + comment: comment, + selectComment: selectCommentIndex[0] == comment.id || + selectCommentIndex[1] == comment.id, + selectReplyIndex: selectReplyIndex[1], + isMine: myEmail == comment.userInformation.email, + myId: myId, + ); + }, + separatorBuilder: (_, index) { + return const SizedBox( + height: 1.0, + ); + }, + ); + } +} diff --git a/frontend/lib/board/view/search_screen.dart b/frontend/lib/board/view/search_screen.dart new file mode 100644 index 0000000000..86cd8f2af8 --- /dev/null +++ b/frontend/lib/board/view/search_screen.dart @@ -0,0 +1,239 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/board/provider/search_state_notifier_provider.dart'; +import 'package:frontend/board/provider/searck_keyword_provider.dart'; + +import '../../common/const/colors.dart'; +import '../../common/model/cursor_pagination_model.dart'; +import '../component/board_card.dart'; +import '../model/msg_board_response_model.dart'; +import '../provider/board_detail_state_notifier_provider.dart'; +import 'msg_board_screen.dart'; + +class SearchScreen extends ConsumerStatefulWidget { + const SearchScreen({super.key}); + + @override + ConsumerState createState() => _SearchScreenState(); +} + +class _SearchScreenState extends ConsumerState { + final ScrollController controller = ScrollController(); + String searchKeyword = ''; + bool isSearched = false; + + @override + void initState() { + super.initState(); + controller.addListener(scrollListener); + } + + @override + void dispose() { + controller.removeListener(scrollListener); + controller.dispose(); + super.dispose(); + } + + void scrollListener() { + if (controller.offset > controller.position.maxScrollExtent - 150) { + ref.read(searchStateNotifierProvider.notifier).paginate(fetchMore: true); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Column( + children: [ + _renderTextFormField(ref, context), + if (!isSearched) + const Expanded( + child: Center( + child: Text( + '궁금한 내용을 검색해보세요!', + style: TextStyle( + fontSize: 16.0, + color: BODY_TEXT_COLOR, + ), + ), + ), + ), + if (isSearched) + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 4.0), + child: _renderSearchedList(), + ), + ), + ], + ), + ), + ); + } + + Widget _renderTextFormField(WidgetRef ref, BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 10, 16, 0), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: TextFormField( + cursorColor: PRIMARY_COLOR, + decoration: InputDecoration( + hintText: '검색어를 입력해주세요.', + hintStyle: const TextStyle( + color: PRIMARY_COLOR, + fontSize: 16.0, + fontWeight: FontWeight.w500, + ), + filled: true, + fillColor: Colors.white, + suffixIcon: IconButton( + icon: const Icon( + Icons.search, + size: 30.0, + ), + color: PRIMARY_COLOR, + onPressed: () => search(ref), + ), + enabledBorder: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + borderSide: BorderSide(color: PRIMARY_COLOR, width: 1.5), + ), + focusedBorder: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0)), + borderSide: BorderSide(color: PRIMARY_COLOR, width: 2.5), + ), + ), + onChanged: (String value) { + setState(() { + searchKeyword = value; + }); + ref.read(searchKeywordProvider.notifier).state = value; + }, + onFieldSubmitted: (String value) => search(ref), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: SizedBox( + height: 60, + child: TextButton( + onPressed: () { + ref.read(searchKeywordProvider.notifier).state = ''; + ref + .read(searchStateNotifierProvider.notifier) + .resetSearchResults(); + setState(() { + searchKeyword = ''; + isSearched = false; + }); + Navigator.of(context).pop(); + }, + child: const Text('취소'), + ), + ), + ), + ], + ), + ], + ), + ); + } + + void search(WidgetRef ref) { + if (searchKeyword.isNotEmpty) { + setState(() { + isSearched = true; + }); + ref + .read(searchStateNotifierProvider.notifier) + .updateAndFetch(searchKeyword); + } + } + + Widget _renderSearchedList() { + final data = ref.watch(searchStateNotifierProvider); + + if (data is CursorPaginationModelLoading) { + return const Center( + child: CircularProgressIndicator( + color: PRIMARY_COLOR, + ), + ); + } + + if (data is CursorPaginationModelError) { + return const Center( + child: Text("데이터를 불러올 수 없습니다."), + ); + } + + final cp = data as CursorPaginationModel; + + if (cp.data.isEmpty) { + return Center( + child: Text("검색 결과가 없습니다.", + style: TextStyle(color: BODY_TEXT_COLOR, fontSize: 16.0), + ), + ); + } + + return RefreshIndicator( + onRefresh: () async { + ref.read(searchStateNotifierProvider.notifier).lastId = + 9223372036854775807; + await ref + .read(searchStateNotifierProvider.notifier) + .paginate(forceRefetch: true); + }, + child: ListView.separated( + controller: controller, + itemCount: cp.data.length + 1, + itemBuilder: (_, index) { + if (index == cp.data.length) { + return Center( + child: cp is CursorPaginationModelFetchingMore + ? const CircularProgressIndicator( + color: PRIMARY_COLOR, + ) + : const Text( + 'Copyright 2024. Decl Team all rights reserved.\n', + style: TextStyle( + color: BODY_TEXT_COLOR, + fontSize: 12.0, + ), + ), + ); + } + + final MsgBoardResponseModel pItem = cp.data[index]; + + return GestureDetector( + child: BoardCard.fromModel(msgBoardResponseModel: pItem), + onTap: () async { + // 상세페이지 + ref.read(boardDetailNotifier.notifier).add(pItem.id); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MsgBoardScreen( + board: pItem, + ), + fullscreenDialog: true), + ); + }, + ); + }, + separatorBuilder: (_, index) { + return const SizedBox(height: 1.0); + }, + ), + ); + } +} diff --git a/frontend/lib/common/component/notice_popup_dialog.dart b/frontend/lib/common/component/notice_popup_dialog.dart new file mode 100644 index 0000000000..c92e62aaee --- /dev/null +++ b/frontend/lib/common/component/notice_popup_dialog.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; + +import '../const/colors.dart'; + +class NoticePopupDialog extends StatelessWidget { + final String message; + final String buttonText; + final VoidCallback onPressed; + final Widget? child; + + const NoticePopupDialog({ + required this.message, + required this.buttonText, + required this.onPressed, + this.child, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + //Dialog 화면의 border + borderRadius: BorderRadius.circular(10.0), + ), + child: Container( + padding: EdgeInsets.all(20.0), + width: MediaQuery.of(context).size.width * 0.7, + height: 200.0, + decoration: BoxDecoration( + border: Border.all(color: Colors.transparent), + borderRadius: + BorderRadius.all(Radius.circular(12.0)), //Dialog 내부 컨테이너의 border + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + message, + style: TextStyle( + fontSize: 16.0, + ), + ), + SizedBox(height: 20.0), + SizedBox( + height: 40.0, + child: ElevatedButton( + onPressed: onPressed, + child: Text( + buttonText, + style: TextStyle( + fontSize: 16.0, + color: Colors.white, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: PRIMARY_COLOR, + textStyle: TextStyle( + color: Colors.white, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/frontend/lib/common/const/colors.dart b/frontend/lib/common/const/colors.dart new file mode 100644 index 0000000000..38f74fd257 --- /dev/null +++ b/frontend/lib/common/const/colors.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; + +const PRIMARY_COLOR = Color(0xFFAA71D8); +const PRIMARY10_COLOR = Color(0xFFF7F1FF); +const PRIMARY20_COLOR = Color(0xFFF8E4FF); +const PRIMARY50_COLOR = Color(0xFFCC96FE); +const BODY_TEXT_COLOR = Color(0xFF868686); +const INPUT_BG_COLOR = Color(0xFFFBFBFB); +const INPUT_BORDER_COLOR = Color(0xFFF3F2F2); diff --git a/frontend/lib/common/const/data.dart b/frontend/lib/common/const/data.dart new file mode 100644 index 0000000000..7ed527eb49 --- /dev/null +++ b/frontend/lib/common/const/data.dart @@ -0,0 +1,11 @@ +import 'ip_list.dart'; + +const ACCESS_TOKEN_KEY = 'ACCESS_TOKEN'; +const REFRESH_TOKEN_KEY = 'REFRESH_TOKEN'; + +const emulatorIp = '10.0.2.2:8080'; +const simulatorIp = '127.0.0.1:8080'; + +// final ip = Platform.isIOS == true ? simulatorIp : emulatorIp; + +const ip = declDomain; diff --git a/frontend/lib/common/dio/dio_custom_interceptor.dart b/frontend/lib/common/dio/dio_custom_interceptor.dart new file mode 100644 index 0000000000..9b4bd5c695 --- /dev/null +++ b/frontend/lib/common/dio/dio_custom_interceptor.dart @@ -0,0 +1,155 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +import '../../member/provider/auth_provider.dart'; +import '../const/data.dart'; + +class CustomInterceptor extends Interceptor { + final FlutterSecureStorage storage; + final Ref ref; + final Dio dio; + + // 리프레쉬 토큰을 새로 발급받고 있으면 대기열에서 기다렸다가 요청을 처리하도록 한다. + bool isRefreshing = false; + List requestQueue = []; + + CustomInterceptor( + this.storage, + this.ref, + this.dio, + ); + + // 1) 요청을 보낼 때 + // 요청이 보내질때마다 요청 Header에 accessToken: true라는 값이 있다면 실제 토큰을 스토리지에서 가져와서 담아서 보내준다. + @override + void onRequest( + RequestOptions options, RequestInterceptorHandler handler) async { + debugPrint('[REQ] [${options.method}] [${options.uri}]'); + + if (options.headers['accessToken'] == 'true') { + options.headers.remove('accessToken'); //헤더 삭제 + + final token = await storage.read(key: ACCESS_TOKEN_KEY); + + options.headers.addAll({ + 'Authorization': 'Bearer $token', //실제 토큰으로 대체 + }); + } + + if (options.headers['refreshToken'] == 'true') { + options.headers.remove('refreshToken'); //헤더 삭제 + + final token = await storage.read(key: REFRESH_TOKEN_KEY); + + options.headers.addAll({ + 'Authorization': 'Bearer $token', //실제 토큰으로 대체 + }); + } + + return super.onRequest(options, handler); + } + + // 2) 응답을 받을 때 + @override + void onResponse(Response response, ResponseInterceptorHandler handler) { + debugPrint( + '[RES] [${response.requestOptions.method}] [${response.requestOptions.uri}]'); + return super.onResponse(response, handler); + } + + // 3) 에러가 났을 때 + @override + void onError(DioException err, ErrorInterceptorHandler handler) async { + // 401 에러가 났을 때 -> 토큰을 재발급받는 시도를 하고, 토큰이 재발급되면 다시 새로운 토큰으로 요청을 한다. + debugPrint( + '[ERR] [${err.requestOptions.method}] [${err.requestOptions.uri}]'); + debugPrint('에러메시지: ${err.message}'); + debugPrint('헤더: ${err.requestOptions.headers}'); + + final refreshToken = await storage.read(key: REFRESH_TOKEN_KEY); + + // refreshToken이 아예 없으면 에러를 던진다. + // 에러를 던질때는 handler.reject를 사용한다. + if (refreshToken == null) { + return handler.reject(err); + } + + final isStatus401 = err.response?.statusCode == 401; + // 나중에 백엔드 예외처리되면 적절하게 수정! + + // 토큰을 새로 발급받으려다가 에러가 난거라면 refreshToken 자체에 문제가 있다! + final isPathRefresh = err.requestOptions.path == '/api/users/reissue-token'; + + if (isStatus401 && !isPathRefresh) { + RequestOptions options = err.requestOptions; + + if (!isRefreshing) { + isRefreshing = true; + getNewToken().then((newTokenAvailable) async { + if (newTokenAvailable) { + final accessToken = await storage.read(key: ACCESS_TOKEN_KEY); + for (var handler in requestQueue) { + options.headers.addAll({ + 'Authorization': 'Bearer $accessToken', + }); + dio.fetch(options).then( + (response) => handler.resolve(response), + onError: (e) => handler.reject(e), + ); + } + } else { + for (var handler in requestQueue) { + handler.reject(err); + } + //로그아웃 처리 + ref.read(authProvider.notifier).logout(); + } + requestQueue.clear(); + isRefreshing = false; + }); + } + //isRefreshing이 true면 대기열에 들어가 기다린다. + requestQueue.add(handler); + } else { + return super.onError(err, handler); + } + } + + Future getNewToken() async { + final refreshToken = await storage.read(key: REFRESH_TOKEN_KEY); + + final dio = Dio(); + debugPrint('보내는 refreshToken: $refreshToken'); + try { + final resp = await dio.post( + '$ip/api/users/reissue-token', + options: Options( + headers: { + 'Authorization': 'Bearer $refreshToken', + }, + ), + ); + + // 새로 받아온 accessToken, refreshToken을 스토리지에 저장 + final newRefreshToken = resp.data['refreshToken']; + final newAccessToken = resp.data['accessToken']; + + debugPrint('받은 refreshToken: $newRefreshToken'); + debugPrint('받은 accessToken: $newAccessToken'); + + if (newRefreshToken == null || newAccessToken == null) { + debugPrint("token null!!!"); + } + + await storage.write(key: REFRESH_TOKEN_KEY, value: newRefreshToken); + await storage.write(key: ACCESS_TOKEN_KEY, value: newAccessToken); + + return true; + } on DioException catch (e) { + debugPrint(e.toString()); + return false; + } + } +} diff --git a/frontend/lib/common/layout/default_layout.dart b/frontend/lib/common/layout/default_layout.dart new file mode 100644 index 0000000000..e2eb840182 --- /dev/null +++ b/frontend/lib/common/layout/default_layout.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class DefaultLayout extends StatelessWidget { + final Color? backgroundColor; + final Widget child; + final Widget? bottomNavigationBar; + + const DefaultLayout({ + this.backgroundColor, + required this.child, + this.bottomNavigationBar, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: backgroundColor ?? Colors.white, + body: child, + bottomNavigationBar: bottomNavigationBar, + ); + } +} diff --git a/frontend/lib/common/model/cursor_pagination_model.dart b/frontend/lib/common/model/cursor_pagination_model.dart new file mode 100644 index 0000000000..36e80d00b4 --- /dev/null +++ b/frontend/lib/common/model/cursor_pagination_model.dart @@ -0,0 +1,91 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'cursor_pagination_model.g.dart'; + +//클래스로 상태를 구분할 때는 항상 base class를 만든다. +abstract class CursorPaginationModelBase {} + +// 응답이 잘 들어왔을 경우 -> CursorPaginationModel +@JsonSerializable( + genericArgumentFactories: true, // 제너릭 사용 옵션 +) +class CursorPaginationModel extends CursorPaginationModelBase { + // 제너릭 -> CursorPaginationModel을 더 유연하게 사용할 수 있게 해준다. + final CursorPaginationMeta meta; + final List data; + + CursorPaginationModel({ + required this.meta, + required this.data, + }); + + CursorPaginationModel copyWith({ + CursorPaginationMeta? meta, + List? data, + }) { + return CursorPaginationModel( + meta: meta ?? this.meta, + data: data ?? this.data, + ); + } + + // T Function(Object? json) fromJsonT -> json을 T타입으로 받을 수 있게 해준다 + // fromJsonT에는 T로 지정해준 타입의 fromJson함수가 들어간다. + factory CursorPaginationModel.fromJson( + Map json, T Function(Object? json) fromJsonT) => + _$CursorPaginationModelFromJson(json, fromJsonT); +} + +@JsonSerializable() +class CursorPaginationMeta { + final int count; + final bool hasMore; + + CursorPaginationMeta( + this.count, + this.hasMore, + ); + + CursorPaginationMeta copyWith({ + int? count, + bool? hasMore, + }) { + return CursorPaginationMeta( + count ?? this.count, + hasMore ?? this.hasMore, + ); + } + + factory CursorPaginationMeta.fromJson(Map json) => + _$CursorPaginationMetaFromJson(json); +} + +// 에러가 났을 경우 -> CursorPaginationModelError +class CursorPaginationModelError extends CursorPaginationModelBase { + final String message; + + CursorPaginationModelError({ + required this.message, + }); +} + +// 로딩중일 경우 -> CursorPaginationModelLoading +class CursorPaginationModelLoading extends CursorPaginationModelBase {} + +// 맨 위에서 당겨서 새로고침했을 경우 -> class CursorPaginationModelRefetching +// 이미 데이터가 있는 상태이므로 CursorPaginationModel을 상속한다. +class CursorPaginationModelRefetching extends CursorPaginationModel { + CursorPaginationModelRefetching({ + required super.meta, + required super.data, + }); +} + +// 맨 아래로 내려서 추가 데이터를 요청하는 경우 -> CursorPaginationFetchingMore +// 이미 데이터가 있는 상태이므로 CursorPaginationModel을 상속한다. +class CursorPaginationModelFetchingMore extends CursorPaginationModel { + CursorPaginationModelFetchingMore({ + required super.meta, + required super.data, + }); +} diff --git a/frontend/lib/common/model/cursor_pagination_model.g.dart b/frontend/lib/common/model/cursor_pagination_model.g.dart new file mode 100644 index 0000000000..54b8db9791 --- /dev/null +++ b/frontend/lib/common/model/cursor_pagination_model.g.dart @@ -0,0 +1,39 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'cursor_pagination_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CursorPaginationModel _$CursorPaginationModelFromJson( + Map json, + T Function(Object? json) fromJsonT, +) => + CursorPaginationModel( + meta: CursorPaginationMeta.fromJson(json['meta'] as Map), + data: (json['data'] as List).map(fromJsonT).toList(), + ); + +Map _$CursorPaginationModelToJson( + CursorPaginationModel instance, + Object? Function(T value) toJsonT, +) => + { + 'meta': instance.meta, + 'data': instance.data.map(toJsonT).toList(), + }; + +CursorPaginationMeta _$CursorPaginationMetaFromJson( + Map json) => + CursorPaginationMeta( + (json['count'] as num).toInt(), + json['hasMore'] as bool, + ); + +Map _$CursorPaginationMetaToJson( + CursorPaginationMeta instance) => + { + 'count': instance.count, + 'hasMore': instance.hasMore, + }; diff --git a/frontend/lib/common/model/login_response.dart b/frontend/lib/common/model/login_response.dart new file mode 100644 index 0000000000..251d479a30 --- /dev/null +++ b/frontend/lib/common/model/login_response.dart @@ -0,0 +1,17 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'login_response.g.dart'; + +@JsonSerializable() +class LoginResponse { + final String accessToken; + final String refreshToken; + + LoginResponse({ + required this.accessToken, + required this.refreshToken, + }); + + factory LoginResponse.fromJson(Map json) + => _$LoginResponseFromJson(json); +} \ No newline at end of file diff --git a/frontend/lib/common/model/login_response.g.dart b/frontend/lib/common/model/login_response.g.dart new file mode 100644 index 0000000000..2bcfec4576 --- /dev/null +++ b/frontend/lib/common/model/login_response.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'login_response.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +LoginResponse _$LoginResponseFromJson(Map json) => + LoginResponse( + accessToken: json['accessToken'] as String, + refreshToken: json['refreshToken'] as String, + ); + +Map _$LoginResponseToJson(LoginResponse instance) => + { + 'accessToken': instance.accessToken, + 'refreshToken': instance.refreshToken, + }; diff --git a/frontend/lib/common/model/token_response.dart b/frontend/lib/common/model/token_response.dart new file mode 100644 index 0000000000..84e63b2d72 --- /dev/null +++ b/frontend/lib/common/model/token_response.dart @@ -0,0 +1,17 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'token_response.g.dart'; + +@JsonSerializable() +class TokenResponse{ + final String accessToken; + final String refreshToken; + + TokenResponse({ + required this.accessToken, + required this.refreshToken, + }); + + factory TokenResponse.fromJson(Map json) + => _$TokenResponseFromJson(json); +} \ No newline at end of file diff --git a/frontend/lib/common/model/token_response.g.dart b/frontend/lib/common/model/token_response.g.dart new file mode 100644 index 0000000000..c82cb8cd2c --- /dev/null +++ b/frontend/lib/common/model/token_response.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'token_response.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +TokenResponse _$TokenResponseFromJson(Map json) => + TokenResponse( + accessToken: json['accessToken'] as String, + refreshToken: json['refreshToken'] as String, + ); + +Map _$TokenResponseToJson(TokenResponse instance) => + { + 'accessToken': instance.accessToken, + 'refreshToken': instance.refreshToken, + }; diff --git a/frontend/lib/common/provider/dio_provider.dart b/frontend/lib/common/provider/dio_provider.dart new file mode 100644 index 0000000000..15f3d582a8 --- /dev/null +++ b/frontend/lib/common/provider/dio_provider.dart @@ -0,0 +1,18 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/common/provider/secure_storage_provider.dart'; + +import '../dio/dio_custom_interceptor.dart'; + +final dioProvider = Provider((ref) { + final dio = Dio(); + + //provider 안에서 다른 provider를 참조할때는 watch를 사용하는 것이 좋다. + final storage = ref.watch(secureStorageProvider); + + dio.interceptors.add( + CustomInterceptor(storage, ref, dio), + ); + + return dio; +}); \ No newline at end of file diff --git a/frontend/lib/common/provider/router_provider.dart b/frontend/lib/common/provider/router_provider.dart new file mode 100644 index 0000000000..383d8eea4a --- /dev/null +++ b/frontend/lib/common/provider/router_provider.dart @@ -0,0 +1,15 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../member/provider/auth_provider.dart'; + +final routerProvider = Provider((ref) { + final provider = ref.read(authProvider); + + return GoRouter( + routes: provider.routes, + initialLocation: '/splash', + refreshListenable: provider, + redirect: provider.redirectLogic, + ); +}); \ No newline at end of file diff --git a/frontend/lib/common/provider/secure_storage_provider.dart b/frontend/lib/common/provider/secure_storage_provider.dart new file mode 100644 index 0000000000..7be046e36a --- /dev/null +++ b/frontend/lib/common/provider/secure_storage_provider.dart @@ -0,0 +1,6 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +final secureStorageProvider = Provider( + (ref) => FlutterSecureStorage(), +); \ No newline at end of file diff --git a/frontend/lib/common/view/splash_screen.dart b/frontend/lib/common/view/splash_screen.dart new file mode 100644 index 0000000000..fedfa09e94 --- /dev/null +++ b/frontend/lib/common/view/splash_screen.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:frontend/common/const/colors.dart'; +import 'package:frontend/common/layout/default_layout.dart'; + +class SplashScreen extends StatelessWidget { + static String get routeName => 'splash'; + + const SplashScreen({super.key}); + + @override + Widget build(BuildContext context) { + return DefaultLayout( + backgroundColor: PRIMARY_COLOR.withOpacity(0.8), + child: SizedBox( + width: MediaQuery.of(context).size.width, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'asset/imgs/logo_white.png', + width: MediaQuery.of(context).size.width / 2, + ), + const SizedBox( + height: 16.0, + ), + const CircularProgressIndicator( + color: Colors.white, + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart new file mode 100644 index 0000000000..2c783ef10e --- /dev/null +++ b/frontend/lib/main.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'common/provider/router_provider.dart'; + +void main() { + runApp( + const ProviderScope( + child: _App(), + ), + ); +} + +class _App extends ConsumerWidget { + const _App({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final router = ref.watch(routerProvider); + + return MaterialApp.router( + routerConfig: router, + theme: ThemeData( + fontFamily: 'NotoSans', + primarySwatch: Colors.purple, + ), + debugShowCheckedModeBanner: false, + ); + } +} diff --git a/frontend/lib/member/component/custom_text_form_field.dart b/frontend/lib/member/component/custom_text_form_field.dart new file mode 100644 index 0000000000..b1fbf048a7 --- /dev/null +++ b/frontend/lib/member/component/custom_text_form_field.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; + +import '../../common/const/colors.dart'; + +class CustomTextFormField extends StatelessWidget { + final String? hintText; + final String? errorText; + final String? suffixText; + final bool obscureText; + final bool isInputEnabled; + final bool autoFocus; + final ValueChanged? onChanged; + + const CustomTextFormField({ + this.hintText, + this.errorText, + this.suffixText, + this.obscureText = false, + this.autoFocus = false, + this.isInputEnabled = true, + required this.onChanged, + super.key, + }); + + @override + Widget build(BuildContext context) { + final baseBorder = OutlineInputBorder( + borderSide: BorderSide( + color: INPUT_BORDER_COLOR, + width: 1.0, + ), + ); + + return TextFormField( + enabled: isInputEnabled, + cursorColor: PRIMARY_COLOR, + // obscureText = 입력할때 비밀번호처럼 가려지게 할지 여부! + obscureText: obscureText, + // autofocus = 화면에 처음 들어왔을때 포커스 시켜놓을지 여부. + autofocus: autoFocus, + onChanged: onChanged, + decoration: InputDecoration( + contentPadding: const EdgeInsets.all(10.0), + suffixIcon: suffixText == null + ? null + : Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text(suffixText!), + ), + suffixIconConstraints: const BoxConstraints( + minHeight: 10, + minWidth: 10, + ), + suffixStyle: const TextStyle( + fontSize: 14.0, + ), + hintText: hintText, + hintStyle: const TextStyle( + color: BODY_TEXT_COLOR, + fontSize: 14.0, + ), + fillColor: INPUT_BG_COLOR, //배경색 + filled: true, //배경색 있음. + // border = 모든 input 상태의 기본 세팅 + border: baseBorder, + //enabledBorder -> + enabledBorder: baseBorder, + focusedBorder: baseBorder.copyWith( + borderSide: baseBorder.borderSide.copyWith( + color: PRIMARY_COLOR, + ), + ), + errorText: errorText, + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/member/model/member_model.dart b/frontend/lib/member/model/member_model.dart new file mode 100644 index 0000000000..5b2147c7d0 --- /dev/null +++ b/frontend/lib/member/model/member_model.dart @@ -0,0 +1,41 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'member_model.g.dart'; + +abstract class MemberModelBase {} + +@JsonSerializable() +class MemberModel extends MemberModelBase { + final int id; + final String name; + final String email; + final String nickname; + final String universityName; + final String major; + final String minor; + final String activatedDepartment; + + MemberModel({ + required this.id, + required this.name, + required this.email, + required this.nickname, + required this.universityName, + required this.major, + required this.minor, + required this.activatedDepartment, + }); + + factory MemberModel.fromJson(Map json) => + _$MemberModelFromJson(json); +} + +class MemberModelLoading extends MemberModelBase {} + +class MemberModelError extends MemberModelBase { + final String message; + + MemberModelError({ + required this.message, + }); +} \ No newline at end of file diff --git a/frontend/lib/member/model/member_model.g.dart b/frontend/lib/member/model/member_model.g.dart new file mode 100644 index 0000000000..16ba8e858d --- /dev/null +++ b/frontend/lib/member/model/member_model.g.dart @@ -0,0 +1,30 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'member_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +MemberModel _$MemberModelFromJson(Map json) => MemberModel( + id: (json['id'] as num).toInt(), + name: json['name'] as String, + email: json['email'] as String, + nickname: json['nickname'] as String, + universityName: json['universityName'] as String, + major: json['major'] as String, + minor: json['minor'] as String, + activatedDepartment: json['activatedDepartment'] as String, + ); + +Map _$MemberModelToJson(MemberModel instance) => + { + 'id': instance.id, + 'name': instance.name, + 'email': instance.email, + 'nickname': instance.nickname, + 'universityName': instance.universityName, + 'major': instance.major, + 'minor': instance.minor, + 'activatedDepartment': instance.activatedDepartment, + }; diff --git a/frontend/lib/member/provider/auth_provider.dart b/frontend/lib/member/provider/auth_provider.dart new file mode 100644 index 0000000000..dacfed44d6 --- /dev/null +++ b/frontend/lib/member/provider/auth_provider.dart @@ -0,0 +1,85 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/board/provider/notification_notifier_provider.dart'; +import 'package:frontend/board/view/msg_board_list_screen.dart'; +import 'package:go_router/go_router.dart'; + +import '../../common/view/splash_screen.dart'; +import '../model/member_model.dart'; +import '../view/login_screen.dart'; +import 'member_state_notifier_provider.dart'; + +final authProvider = ChangeNotifierProvider((ref) { + return AuthProvider(ref: ref); +}); + +class AuthProvider extends ChangeNotifier { + final Ref ref; + + AuthProvider({ + required this.ref, + }) { + //memberStateNotifierProvider를 listen하면 유저의 로그인 상태를 알 수 있다. + ref.listen(memberStateNotifierProvider, (previous, next) { + if (previous != next) { + // 변경사항이 있을때만 + notifyListeners(); + } + }); + } + + List get routes => [ + GoRoute( + path: '/boardList', + name: MsgBoardListScreen.routeName, + builder: (context, state) => const MsgBoardListScreen(), + ), + GoRoute( + path: '/splash', + name: SplashScreen.routeName, + builder: (context, state) => const SplashScreen(), + ), + GoRoute( + path: '/login', + name: LoginScreen.routeName, + builder: (context, state) => const LoginScreen(), + ), + ]; + + void logout() { + ref.read(memberStateNotifierProvider.notifier).logout(); + } + + String? redirectLogic(BuildContext context, GoRouterState state) { + final MemberModelBase? member = ref.read(memberStateNotifierProvider); + + final loggingIn = state.location == '/login'; + + // 유저정보가 없는데 로그인 중이면 그대로 두고 + // 로그인 중이 아니라면 로그인 페이지로 이동. + if (member == null) { + print("member is null!!!"); + return loggingIn ? null : '/login'; + } + + // member가 null이 아니다. (사용자 정보가 있는 상태) + + // 1. MemberModel + // 로그인 중이거나 현재 위치가 SplashScreen이면 홈으로 이동 + if (member is MemberModel) { + if (loggingIn || state.location == '/splash') { + ref.watch(notificationStateProvider.notifier).listen(0); + return '/boardList'; + } + } + + // 2. MemberModelError + // 로그인 하던게 아니라면 로그인 페이지로 이동 + if (member is MemberModelError) { + return !loggingIn ? '/login' : null; + } + + // 나머지는 원래 가던곳으로 가라! + return null; + } +} diff --git a/frontend/lib/member/provider/auth_repository_provider.dart b/frontend/lib/member/provider/auth_repository_provider.dart new file mode 100644 index 0000000000..03cfb57f24 --- /dev/null +++ b/frontend/lib/member/provider/auth_repository_provider.dart @@ -0,0 +1,55 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../common/const/data.dart'; +import '../../common/model/login_response.dart'; +import '../../common/model/token_response.dart'; +import '../../common/provider/dio_provider.dart'; + +final authRepositoryProvider = Provider((ref) { + final dio = ref.watch(dioProvider); + + return AuthRepository( + baseUrl: ip, + dio: dio, + ); +}); + +class AuthRepository { + final String baseUrl; + final Dio dio; + + AuthRepository({ + required this.baseUrl, + required this.dio, + }); + + Future login({ + required String email, + required String password, + }) async { + final resp = await dio.post( + '$baseUrl/api/users/login', + data: {'email': email, 'password': password}, + options: Options( + headers: { + 'Content-Type': 'application/json', + }, + ), + ); + return LoginResponse.fromJson(resp.data); + } + + Future token() async { + final resp = await dio.post( + '$baseUrl/api/users/reissue-token', + options: Options( + headers: { + 'refreshToken': 'true', + }, + ), + ); + + return TokenResponse.fromJson(resp.data); + } +} \ No newline at end of file diff --git a/frontend/lib/member/provider/first_major_state_notifier_provider.dart b/frontend/lib/member/provider/first_major_state_notifier_provider.dart new file mode 100644 index 0000000000..5b621c13fa --- /dev/null +++ b/frontend/lib/member/provider/first_major_state_notifier_provider.dart @@ -0,0 +1,76 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +// 1. FutureProvider를 사용하여 DepartmentState를 로드하는 함수 + +final firstDepartmentStateProvider = + FutureProvider((ref) async { + String jsonString = + await rootBundle.loadString('asset/jsons/departments.json'); + Map divisionAndDepartments = json.decode(jsonString); + + // 초기에 선택되어 있을 계열과 학과를 설정! + String selectedDivision = divisionAndDepartments.keys.first; + String selectedDepartment = divisionAndDepartments[selectedDivision][0]; + + return FirstDepartmentState( + divisionAndDepartments: divisionAndDepartments, + selectedDivision: selectedDivision, + selectedDepartment: selectedDepartment, + ); +}); + +class FirstDepartmentState { + final Map divisionAndDepartments; + String selectedDivision; + String selectedDepartment; + + FirstDepartmentState({ + required this.divisionAndDepartments, + required this.selectedDivision, + required this.selectedDepartment, + }); + + FirstDepartmentState copyWith({ + Map? divisionAndDepartments, + String? selectedDivision, + String? selectedDepartment, + }) { + return FirstDepartmentState( + divisionAndDepartments: + divisionAndDepartments ?? this.divisionAndDepartments, + selectedDivision: selectedDivision ?? this.selectedDivision, + selectedDepartment: selectedDepartment ?? this.selectedDepartment, + ); + } +} + +// 2. StateNotifierProvider를 사용하여 DepartmentState를 관리하는 DepartmentListNotifierProvider + +final firstDepartmentListNotifierProvider = + StateNotifierProvider((ref) { + // FutureProvider에서 로드된 초기 상태를 사용하여 StateNotifier를 생성 + final initialState = ref.watch(firstDepartmentStateProvider).asData?.value ?? + FirstDepartmentState( + divisionAndDepartments: {}, + selectedDivision: '', + selectedDepartment: '', + ); + + return FirstDepartmentListNotifier(initialState); +}); + +class FirstDepartmentListNotifier extends StateNotifier { + FirstDepartmentListNotifier(FirstDepartmentState state) : super(state); + + // 선택된 계열이나 학과를 변경하는 메서드 + void setSelectedDivision(String division) { + state = state.copyWith(selectedDivision: division); + } + + void setSelectedDepartment(String department) { + state = state.copyWith(selectedDepartment: department); + } +} diff --git a/frontend/lib/member/provider/member_repository_provider.dart b/frontend/lib/member/provider/member_repository_provider.dart new file mode 100644 index 0000000000..ad5c53083c --- /dev/null +++ b/frontend/lib/member/provider/member_repository_provider.dart @@ -0,0 +1,37 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../common/const/data.dart'; +import '../../common/provider/dio_provider.dart'; +import '../model/member_model.dart'; + +final memberRepositoryProvider = Provider( + (ref) { + final dio = ref.watch(dioProvider); + + return MemberRepository(baseUrl: ip, dio: dio); + }, +); + +class MemberRepository{ + final String baseUrl; + final Dio dio; + + MemberRepository({ + required this.baseUrl, + required this.dio, + }); + + Future getMe() async { + final resp = await dio.get( + '$baseUrl/api/users/me', + options: Options( + headers: { + 'accessToken': 'true', + }, + ), + ); + + return MemberModel.fromJson(resp.data); + } +} \ No newline at end of file diff --git a/frontend/lib/member/provider/member_state_notifier_provider.dart b/frontend/lib/member/provider/member_state_notifier_provider.dart new file mode 100644 index 0000000000..330e7fbda4 --- /dev/null +++ b/frontend/lib/member/provider/member_state_notifier_provider.dart @@ -0,0 +1,112 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_client_sse/flutter_client_sse.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +import '../../common/const/data.dart'; +import '../../common/provider/secure_storage_provider.dart'; +import '../model/member_model.dart'; +import 'auth_repository_provider.dart'; +import 'member_repository_provider.dart'; +import 'mypage/my_comment_state_notifier_provider.dart'; +import 'mypage/my_post_state_notifier_provider.dart'; +import 'mypage/my_scrap_state_notifier_provider.dart'; + +final memberStateNotifierProvider = + StateNotifierProvider((ref) { + final authRepository = ref.watch(authRepositoryProvider); + final memberRepository = ref.watch(memberRepositoryProvider); + final storage = ref.watch(secureStorageProvider); + + return MemberStateNotifier( + authRepository: authRepository, + memberRepository: memberRepository, + storage: storage, + ref: ref, + ); +}); + +class MemberStateNotifier extends StateNotifier { + final AuthRepository authRepository; + final MemberRepository memberRepository; + final FlutterSecureStorage storage; + final Ref ref; + + MemberStateNotifier({ + required this.authRepository, + required this.memberRepository, + required this.storage, + required this.ref, + }) : super(MemberModelLoading()) { + //내 정보 가져오기 + getMe(); + } + + Future getMe() async { + try { + final accessToken = await storage.read(key: ACCESS_TOKEN_KEY); + final refreshToken = await storage.read(key: REFRESH_TOKEN_KEY); + + if (refreshToken == null || accessToken == null) { + state = null; + return; + } + + final resp = await memberRepository.getMe(); + + //성공적으로 가져왔을 경우 MemberModel이 state에 담기게 된다. + state = resp; + } on DioException catch (e) { + if (e.response?.statusCode == 401) { + state = MemberModelError(message: "로그인되지 않았습니다."); + } else { + state = MemberModelError(message: "기타 에러..!"); + } + } + } + + Future login({ + required String email, + required String password, + }) async { + try { + state = MemberModelLoading(); + + final resp = await authRepository.login( + email: email, + password: password, + ); //resp는 LoginResponse이다. + + await storage.write(key: REFRESH_TOKEN_KEY, value: resp.refreshToken); + await storage.write(key: ACCESS_TOKEN_KEY, value: resp.accessToken); + + final memberResp = await memberRepository.getMe(); + state = memberResp; + + ref.read(myPostStateNotifierProvider.notifier).fetchData(); + ref.read(myCommentStateNotifierProvider.notifier).fetchData(); + ref.read(myScrapStateNotifierProvider.notifier).fetchData(); + + return memberResp; + } catch (e) { + state = MemberModelError(message: '로그인에 실패했습니다.'); + return Future.value(state); + } + } + + Future logout() async { + state = null; + + await Future.wait( + [ + storage.delete(key: REFRESH_TOKEN_KEY), + storage.delete(key: ACCESS_TOKEN_KEY), + ], + ); + + ref.read(myPostStateNotifierProvider.notifier).clearData(); + ref.read(myCommentStateNotifierProvider.notifier).clearData(); + ref.read(myScrapStateNotifierProvider.notifier).clearData(); + SSEClient.unsubscribeFromSSE(); + } +} diff --git a/frontend/lib/member/provider/my_page_repository_provider.dart b/frontend/lib/member/provider/my_page_repository_provider.dart new file mode 100644 index 0000000000..f6a682d577 --- /dev/null +++ b/frontend/lib/member/provider/my_page_repository_provider.dart @@ -0,0 +1,48 @@ +import 'package:dio/dio.dart' hide Headers; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/common/provider/dio_provider.dart'; +import 'package:retrofit/http.dart'; + +import '../../board/model/msg_board_response_model.dart'; +import '../../common/const/data.dart'; +import '../../common/model/cursor_pagination_model.dart'; + +part 'my_page_repository_provider.g.dart'; + +final myPageRepositoryProvider = Provider((ref) { + final dio = ref.watch(dioProvider); + + return MyPageRepository(dio, baseUrl: ip); +}); + +@RestApi() +abstract class MyPageRepository { + factory MyPageRepository(Dio dio, {String baseUrl}) = _MyPageRepository; + + @GET('/api/post/mine') + @Headers({ + 'accessToken': 'true', + }) + Future> getMyPosts( + @Query('lastId') int lastId, + @Query('size') int size, + ); + + @GET('/api/post/commented') + @Headers({ + 'accessToken': 'true', + }) + Future> getCommentedPosts( + @Query('lastId') int lastId, + @Query('size') int size, + ); + + @GET('/api/post/scrapped') + @Headers({ + 'accessToken': 'true', + }) + Future> getScrappedPosts( + @Query('lastId') int lastId, + @Query('size') int size, + ); +} diff --git a/frontend/lib/member/provider/my_page_repository_provider.g.dart b/frontend/lib/member/provider/my_page_repository_provider.g.dart new file mode 100644 index 0000000000..1baf835581 --- /dev/null +++ b/frontend/lib/member/provider/my_page_repository_provider.g.dart @@ -0,0 +1,161 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'my_page_repository_provider.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers + +class _MyPageRepository implements MyPageRepository { + _MyPageRepository( + this._dio, { + this.baseUrl, + }); + + final Dio _dio; + + String? baseUrl; + + @override + Future> getMyPosts( + int lastId, + int size, + ) async { + const _extra = {}; + final queryParameters = { + r'lastId': lastId, + r'size': size, + }; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final Map? _data = null; + final _result = await _dio.fetch>( + _setStreamType>(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/post/mine', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = CursorPaginationModel.fromJson( + _result.data!, + (json) => MsgBoardResponseModel.fromJson(json as Map), + ); + return value; + } + + @override + Future> getCommentedPosts( + int lastId, + int size, + ) async { + const _extra = {}; + final queryParameters = { + r'lastId': lastId, + r'size': size, + }; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final Map? _data = null; + final _result = await _dio.fetch>( + _setStreamType>(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/post/commented', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = CursorPaginationModel.fromJson( + _result.data!, + (json) => MsgBoardResponseModel.fromJson(json as Map), + ); + return value; + } + + @override + Future> getScrappedPosts( + int lastId, + int size, + ) async { + const _extra = {}; + final queryParameters = { + r'lastId': lastId, + r'size': size, + }; + final _headers = {r'accessToken': 'true'}; + _headers.removeWhere((k, v) => v == null); + final Map? _data = null; + final _result = await _dio.fetch>( + _setStreamType>(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/api/post/scrapped', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = CursorPaginationModel.fromJson( + _result.data!, + (json) => MsgBoardResponseModel.fromJson(json as Map), + ); + return value; + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls( + String dioBaseUrl, + String? baseUrl, + ) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/frontend/lib/member/provider/mypage/my_comment_state_notifier_provider.dart b/frontend/lib/member/provider/mypage/my_comment_state_notifier_provider.dart new file mode 100644 index 0000000000..0e64460813 --- /dev/null +++ b/frontend/lib/member/provider/mypage/my_comment_state_notifier_provider.dart @@ -0,0 +1,125 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../common/model/cursor_pagination_model.dart'; +import '../my_page_repository_provider.dart'; + +final myCommentStateNotifierProvider = +StateNotifierProvider( + (ref) { + final repository = ref.watch(myPageRepositoryProvider); + const initialLastPostId = 9223372036854775807; + const size = 20; + + final notifier = MyCommentStateNotifier( + repository: repository, + lastId: initialLastPostId, + size: size, + ); + + return notifier; + }); + +class MyCommentStateNotifier extends StateNotifier { + bool _mounted = true; + bool _fetchingData = false; + + void clearData() { + state = CursorPaginationModelLoading(); + } + + void fetchData() { + paginate(forceRefetch: true); + } + + @override + void dispose() { + _mounted = false; + super.dispose(); + } + + final MyPageRepository repository; + int lastId; + int size; + + MyCommentStateNotifier({ + required this.repository, + required this.lastId, + required this.size, + }) : super(CursorPaginationModelLoading()) { + paginate(); + } + + bool get isMounted => _mounted; + + Future paginate({ + bool fetchMore = false, + bool forceRefetch = false, + }) async { + if (!isMounted) return; + + if (_fetchingData) return; + _fetchingData = true; + + try { + if (state is CursorPaginationModel && !forceRefetch) { + final pState = state as CursorPaginationModel; + + if (!pState.meta.hasMore) { + return; + } + } + + final isLoading = state is CursorPaginationModelLoading; + final isRefetching = state is CursorPaginationModelRefetching; + final isFetchingMore = state is CursorPaginationModelFetchingMore; + + if (fetchMore && (isLoading || isRefetching || isFetchingMore)) { + return; + } + + if (fetchMore) { + final pState = (state as CursorPaginationModel); // 무조건 데이터를 들고있는 상황 + + state = CursorPaginationModelFetchingMore( + meta: pState.meta, + data: pState.data, + ); + lastId = pState.data.last.id; + } else { + if (state is CursorPaginationModel && !forceRefetch) { + final pState = state as CursorPaginationModel; + + state = CursorPaginationModelRefetching( + meta: pState.meta, + data: pState.data, + ); + } else { + state = CursorPaginationModelLoading(); + } + } + final resp = await repository.getCommentedPosts(lastId, size); + + if (!isMounted) return; + + if (state is CursorPaginationModelFetchingMore) { + final pState = state as CursorPaginationModelFetchingMore; + state = resp.copyWith( + data: [ + ...pState.data, + ...resp.data, + ], + ); + } else { + state = resp; + } + } catch (e) { + if (!isMounted) return; + + print(e.runtimeType); + + state = CursorPaginationModelError(message: '데이터를 가져오지 못했습니다'); + } finally { + _fetchingData = false; + } + } +} diff --git a/frontend/lib/member/provider/mypage/my_post_state_notifier_provider.dart b/frontend/lib/member/provider/mypage/my_post_state_notifier_provider.dart new file mode 100644 index 0000000000..9664d588b6 --- /dev/null +++ b/frontend/lib/member/provider/mypage/my_post_state_notifier_provider.dart @@ -0,0 +1,124 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/common/model/cursor_pagination_model.dart'; +import 'package:frontend/member/provider/my_page_repository_provider.dart'; + +final myPostStateNotifierProvider = + StateNotifierProvider( + (ref) { + final repository = ref.watch(myPageRepositoryProvider); + const initialLastPostId = 9223372036854775807; + const size = 20; + + final notifier = MyPostStateNotifier( + repository: repository, + lastId: initialLastPostId, + size: size, + ); + + return notifier; +}); + +class MyPostStateNotifier extends StateNotifier { + bool _mounted = true; + bool _fetchingData = false; + + void clearData() { + state = CursorPaginationModelLoading(); + } + + void fetchData() { + paginate(forceRefetch: true); + } + + @override + void dispose() { + _mounted = false; + super.dispose(); + } + + final MyPageRepository repository; + int lastId; + int size; + + MyPostStateNotifier({ + required this.repository, + required this.lastId, + required this.size, + }) : super(CursorPaginationModelLoading()) { + paginate(); + } + + bool get isMounted => _mounted; + + Future paginate({ + bool fetchMore = false, + bool forceRefetch = false, + }) async { + if (!isMounted) return; + + if (_fetchingData) return; + _fetchingData = true; + + try { + if (state is CursorPaginationModel && !forceRefetch) { + final pState = state as CursorPaginationModel; + + if (!pState.meta.hasMore) { + return; + } + } + + final isLoading = state is CursorPaginationModelLoading; + final isRefetching = state is CursorPaginationModelRefetching; + final isFetchingMore = state is CursorPaginationModelFetchingMore; + + if (fetchMore && (isLoading || isRefetching || isFetchingMore)) { + return; + } + + if (fetchMore) { + final pState = (state as CursorPaginationModel); // 무조건 데이터를 들고있는 상황 + + state = CursorPaginationModelFetchingMore( + meta: pState.meta, + data: pState.data, + ); + lastId = pState.data.last.id; + } else { + if (state is CursorPaginationModel && !forceRefetch) { + final pState = state as CursorPaginationModel; + + state = CursorPaginationModelRefetching( + meta: pState.meta, + data: pState.data, + ); + } else { + state = CursorPaginationModelLoading(); + } + } + final resp = await repository.getMyPosts(lastId, size); + + if (!isMounted) return; + + if (state is CursorPaginationModelFetchingMore) { + final pState = state as CursorPaginationModelFetchingMore; + state = resp.copyWith( + data: [ + ...pState.data, + ...resp.data, + ], + ); + } else { + state = resp; + } + } catch (e) { + if (!isMounted) return; + + print(e.runtimeType); + + state = CursorPaginationModelError(message: '데이터를 가져오지 못했습니다'); + } finally { + _fetchingData = false; + } + } +} diff --git a/frontend/lib/member/provider/mypage/my_scrap_state_notifier_provider.dart b/frontend/lib/member/provider/mypage/my_scrap_state_notifier_provider.dart new file mode 100644 index 0000000000..17bdd166f2 --- /dev/null +++ b/frontend/lib/member/provider/mypage/my_scrap_state_notifier_provider.dart @@ -0,0 +1,125 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../common/model/cursor_pagination_model.dart'; +import '../my_page_repository_provider.dart'; + +final myScrapStateNotifierProvider = +StateNotifierProvider( + (ref) { + final repository = ref.watch(myPageRepositoryProvider); + const initialLastPostId = 9223372036854775807; + const size = 20; + + final notifier = MyScrapStateNotifier( + repository: repository, + lastId: initialLastPostId, + size: size, + ); + + return notifier; + }); + +class MyScrapStateNotifier extends StateNotifier { + bool _mounted = true; + bool _fetchingData = false; + + void clearData() { + state = CursorPaginationModelLoading(); + } + + void fetchData() { + paginate(forceRefetch: true); + } + + @override + void dispose() { + _mounted = false; + super.dispose(); + } + + final MyPageRepository repository; + int lastId; + int size; + + MyScrapStateNotifier({ + required this.repository, + required this.lastId, + required this.size, + }) : super(CursorPaginationModelLoading()) { + paginate(); + } + + bool get isMounted => _mounted; + + Future paginate({ + bool fetchMore = false, + bool forceRefetch = false, + }) async { + if (!isMounted) return; + + if (_fetchingData) return; + _fetchingData = true; + + try { + if (state is CursorPaginationModel && !forceRefetch) { + final pState = state as CursorPaginationModel; + + if (!pState.meta.hasMore) { + return; + } + } + + final isLoading = state is CursorPaginationModelLoading; + final isRefetching = state is CursorPaginationModelRefetching; + final isFetchingMore = state is CursorPaginationModelFetchingMore; + + if (fetchMore && (isLoading || isRefetching || isFetchingMore)) { + return; + } + + if (fetchMore) { + final pState = (state as CursorPaginationModel); // 무조건 데이터를 들고있는 상황 + + state = CursorPaginationModelFetchingMore( + meta: pState.meta, + data: pState.data, + ); + lastId = pState.data.last.id; + } else { + if (state is CursorPaginationModel && !forceRefetch) { + final pState = state as CursorPaginationModel; + + state = CursorPaginationModelRefetching( + meta: pState.meta, + data: pState.data, + ); + } else { + state = CursorPaginationModelLoading(); + } + } + final resp = await repository.getScrappedPosts(lastId, size); + + if (!isMounted) return; + + if (state is CursorPaginationModelFetchingMore) { + final pState = state as CursorPaginationModelFetchingMore; + state = resp.copyWith( + data: [ + ...pState.data, + ...resp.data, + ], + ); + } else { + state = resp; + } + } catch (e) { + if (!isMounted) return; + + print(e.runtimeType); + + state = CursorPaginationModelError(message: '데이터를 가져오지 못했습니다'); + } finally { + _fetchingData = false; + } + } +} diff --git a/frontend/lib/member/provider/second_major_state_notifier_provider.dart b/frontend/lib/member/provider/second_major_state_notifier_provider.dart new file mode 100644 index 0000000000..4746e04587 --- /dev/null +++ b/frontend/lib/member/provider/second_major_state_notifier_provider.dart @@ -0,0 +1,76 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +// 1. FutureProvider를 사용하여 DepartmentState를 로드하는 함수 + +final secondDepartmentStateProvider = + FutureProvider((ref) async { + String jsonString = + await rootBundle.loadString('asset/jsons/departments.json'); + Map divisionAndDepartments = json.decode(jsonString); + + // 초기에 선택되어 있을 계열과 학과를 설정! + String selectedDivision = divisionAndDepartments.keys.first; + String selectedDepartment = divisionAndDepartments[selectedDivision][0]; + + return SecondDepartmentState( + divisionAndDepartments: divisionAndDepartments, + selectedDivision: selectedDivision, + selectedDepartment: selectedDepartment, + ); +}); + +class SecondDepartmentState { + final Map divisionAndDepartments; + String selectedDivision; + String selectedDepartment; + + SecondDepartmentState({ + required this.divisionAndDepartments, + required this.selectedDivision, + required this.selectedDepartment, + }); + + SecondDepartmentState copyWith({ + Map? divisionAndDepartments, + String? selectedDivision, + String? selectedDepartment, + }) { + return SecondDepartmentState( + divisionAndDepartments: + divisionAndDepartments ?? this.divisionAndDepartments, + selectedDivision: selectedDivision ?? this.selectedDivision, + selectedDepartment: selectedDepartment ?? this.selectedDepartment, + ); + } +} + +// 2. StateNotifierProvider를 사용하여 DepartmentState를 관리하는 DepartmentListNotifierProvider + +final secondDepartmentListNotifierProvider = + StateNotifierProvider((ref) { + // FutureProvider에서 로드된 초기 상태를 사용하여 StateNotifier를 생성 + final initialState = ref.watch(secondDepartmentStateProvider).asData?.value ?? + SecondDepartmentState( + divisionAndDepartments: {}, + selectedDivision: '', + selectedDepartment: '', + ); + + return SecondDepartmentListNotifier(initialState); +}); + +class SecondDepartmentListNotifier extends StateNotifier { + SecondDepartmentListNotifier(SecondDepartmentState state) : super(state); + + // 선택된 계열이나 학과를 변경하는 메서드 + void setSelectedDivision(String division) { + state = state.copyWith(selectedDivision: division); + } + + void setSelectedDepartment(String department) { + state = state.copyWith(selectedDepartment: department); + } +} diff --git a/frontend/lib/member/view/login_screen.dart b/frontend/lib/member/view/login_screen.dart new file mode 100644 index 0000000000..7dffd4c39f --- /dev/null +++ b/frontend/lib/member/view/login_screen.dart @@ -0,0 +1,256 @@ +import 'package:app_version_update/app_version_update.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/member/view/password_reset_screen.dart'; +import 'package:frontend/member/view/signup_screen.dart'; + +import '../../common/component/notice_popup_dialog.dart'; +import '../../common/const/colors.dart'; +import '../../common/layout/default_layout.dart'; +import '../component/custom_text_form_field.dart'; +import '../model/member_model.dart'; +import '../provider/member_state_notifier_provider.dart'; + +class LoginScreen extends ConsumerStatefulWidget { + static String get routeName => 'login'; + + const LoginScreen({super.key}); + + @override + ConsumerState createState() => _LoginScreenState(); +} + +class _LoginScreenState extends ConsumerState { + bool isWillingToResetPassword = false; + String email = ''; + String password = ''; + + void getNoticeDialog(BuildContext context, String message) { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: message, + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); + }, + ); + }, + ); + } + + @override + void initState() { + _verifyVersion(); + super.initState(); + } + + void _verifyVersion() async { + await AppVersionUpdate.checkForUpdates( + appleId: '6499332881', + playStoreId: 'com.capstone.decl', + country: 'kr', + ).then((result) async { + if (result.canUpdate!) { + // await AppVersionUpdate.showBottomSheetUpdate(context: context, appVersionResult: appVersionResult) + // await AppVersionUpdate.showPageUpdate(context: context, appVersionResult: appVersionResult) + // or use your own widget with information received from AppVersionResult + + //############################################################################################## + await AppVersionUpdate.showAlertUpdate( + appVersionResult: result, + context: context, + mandatory: true, + backgroundColor: Colors.grey[200], + title: '새로운 업데이트', + titleTextStyle: const TextStyle( + color: Colors.black, fontWeight: FontWeight.w600, fontSize: 24.0), + content: '새로운 업데이트가 있습니다.', + contentTextStyle: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.w400, + ), + updateButtonText: '다운로드', + updateTextStyle: const TextStyle( + color: Colors.white, + ), + updateButtonStyle: ButtonStyle( + backgroundColor: const MaterialStatePropertyAll(PRIMARY_COLOR), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + ), + cancelButtonText: 'LATER', + ); + } + }); + } + + @override + Widget build(BuildContext context) { + final state = ref.watch(memberStateNotifierProvider); + + if (state is MemberModelError) { + WidgetsBinding.instance.addPostFrameCallback((_) { + getNoticeDialog(context, state.message); + }); + } + + return DefaultLayout( + child: SingleChildScrollView( + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + child: SafeArea( + top: true, + bottom: false, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 20.0), + child: Center( + child: Image.asset( + 'asset/imgs/logo.png', + width: 80.0, + ), + ), + ), + _renderTitle(), + _renderSubTitle(), + const SizedBox(height: 16.0), + Padding( + padding: const EdgeInsets.fromLTRB(12.0, 50.0, 12.0, 50.0), + child: SizedBox( + width: 220.0, + height: 220.0, + child: Image.asset( + 'asset/imgs/decle.png', + ), + ), + ), + CustomTextFormField( + hintText: '이메일을 입력해주세요.', + onChanged: (String value) { + email = value; + }, + ), + const SizedBox(height: 16.0), + CustomTextFormField( + hintText: '비밀번호를 입력해주세요.', + onChanged: (String value) { + password = value; + }, + obscureText: true, + ), + const SizedBox(height: 16.0), + SizedBox( + height: 40.0, + child: ElevatedButton( + onPressed: state is MemberModelLoading //로딩중이면 로그인 버튼 못누르도록 + ? null + : () async { + ref + .read(memberStateNotifierProvider.notifier) + .login(email: email, password: password); + }, + style: ElevatedButton.styleFrom( + backgroundColor: PRIMARY_COLOR, + textStyle: const TextStyle( + color: Colors.white, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + child: const Text( + '로그인', + style: TextStyle( + color: Colors.white, + ), + ), + ), + ), + const SizedBox(height: 8.0), + SizedBox( + height: 40.0, + child: TextButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const SignupScreen(), + ), + ); + }, + style: ButtonStyle( + foregroundColor: MaterialStateProperty.all(PRIMARY_COLOR), + side: MaterialStateProperty.all( + const BorderSide( + color: PRIMARY_COLOR, + width: 1.0, + ), + ), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + ), + child: const Text('회원가입'), + ), + ), + const SizedBox(height: 8.0), + SizedBox( + height: 40.0, + child: TextButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const PasswordResetScreen(), + ), + ); + }, + style: ButtonStyle( + foregroundColor: + MaterialStateProperty.all(BODY_TEXT_COLOR), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + ), + child: const Text('비밀번호 초기화'), + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _renderTitle() { + return const Text( + '학과별 정보공유 커뮤니티, 디클', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ); + } + + Widget _renderSubTitle() { + return const Text( + '학교 이메일과 비밀번호를 입력해서 로그인 해주세요!', + style: TextStyle( + fontSize: 16, + color: BODY_TEXT_COLOR, + ), + ); + } +} diff --git a/frontend/lib/member/view/my_comment_screen.dart b/frontend/lib/member/view/my_comment_screen.dart new file mode 100644 index 0000000000..888f865ae7 --- /dev/null +++ b/frontend/lib/member/view/my_comment_screen.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/member/provider/mypage/my_comment_state_notifier_provider.dart'; + +import '../../board/component/board_card.dart'; +import '../../board/model/msg_board_response_model.dart'; +import '../../board/provider/board_detail_state_notifier_provider.dart'; +import '../../board/view/msg_board_screen.dart'; +import '../../common/const/colors.dart'; +import '../../common/model/cursor_pagination_model.dart'; + +class MyCommentScreen extends ConsumerStatefulWidget { + const MyCommentScreen({super.key}); + + @override + ConsumerState createState() => _MyCommentScreenState(); +} + +class _MyCommentScreenState extends ConsumerState { + final ScrollController controller = ScrollController(); + + @override + void initState() { + super.initState(); + controller.addListener(scrollListener); + } + + void scrollListener() { + if (controller.offset > controller.position.maxScrollExtent - 150) { + ref.read(myCommentStateNotifierProvider.notifier).paginate( + fetchMore: true, + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Column( + children: [ + _renderTop(), + Expanded( + child: _renderMyCommentedPostList(), + ), + ], + ), + ), + ); + } + + Widget _renderTop() { + return Container( + child: Stack( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + IconButton( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: const Icon( + Icons.chevron_left, + ), + ), + ], + ), + const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 12.0), + child: Text( + "댓글단 글", + style: TextStyle( + fontSize: 16.0, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _renderMyCommentedPostList() { + final data = ref.watch(myCommentStateNotifierProvider); + + if (data is CursorPaginationModelLoading) { + return const Center( + child: CircularProgressIndicator( + color: PRIMARY_COLOR, + ), + ); + } + + if (data is CursorPaginationModelError) { + return const Center( + child: Text("데이터를 불러올 수 없습니다."), + ); + } + + final cp = data as CursorPaginationModel; + + return RefreshIndicator( + onRefresh: () async { + ref.read(myCommentStateNotifierProvider.notifier).lastId = + 9223372036854775807; + await ref + .read(myCommentStateNotifierProvider.notifier) + .paginate(forceRefetch: true); + }, + child: ListView.separated( + controller: controller, + itemCount: cp.data.length + 1, + itemBuilder: (_, index) { + if (index == cp.data.length) { + return Center( + child: cp is CursorPaginationModelFetchingMore + ? const CircularProgressIndicator( + color: PRIMARY_COLOR, + ) + : const Text( + 'Copyright 2024. Decl Team all rights reserved.\n', + style: TextStyle( + color: BODY_TEXT_COLOR, + fontSize: 12.0, + ), + ), + ); + } + + final MsgBoardResponseModel pItem = cp.data[index]; + + return GestureDetector( + child: BoardCard.fromModel(msgBoardResponseModel: pItem), + onTap: () async { + // 상세페이지 + ref.read(boardDetailNotifier.notifier).add(pItem.id); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MsgBoardScreen( + board: pItem, + ), + fullscreenDialog: true), + ); + }, + ); + }, + separatorBuilder: (_, index) { + return const SizedBox(height: 1.0); + }, + ), + ); + } +} diff --git a/frontend/lib/member/view/my_info_screen.dart b/frontend/lib/member/view/my_info_screen.dart new file mode 100644 index 0000000000..03c884b742 --- /dev/null +++ b/frontend/lib/member/view/my_info_screen.dart @@ -0,0 +1,522 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/common/const/colors.dart'; +import 'package:frontend/common/provider/dio_provider.dart'; + +import '../../common/component/notice_popup_dialog.dart'; +import '../../common/const/data.dart'; +import '../model/member_model.dart'; +import '../provider/first_major_state_notifier_provider.dart'; +import '../provider/member_state_notifier_provider.dart'; +import '../provider/second_major_state_notifier_provider.dart'; + +class MyInfoScreen extends ConsumerStatefulWidget { + const MyInfoScreen({super.key}); + + @override + ConsumerState createState() => _MyInfoScreenState(); +} + +class _MyInfoScreenState extends ConsumerState { + bool isWillingToChangeMajor = false; + String selectedMajor = ""; + String selectedMinor = ""; + + @override + Widget build(BuildContext context) { + final dio = ref.watch(dioProvider); + + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: SingleChildScrollView( + child: Column( + children: [ + _renderTop(), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 18.0, vertical: 10.0), + child: Column( + children: [ + _renderMyInfo(), + if (!isWillingToChangeMajor) + _renderButton( + "전공 변경하기", + () async { + try { + final resp = await dio.get( + '$ip/api/belongs/remain', + options: Options( + headers: { + 'accessToken': 'true', + }, + ), + ); + if (resp.statusCode == 200) { + int remainDays = resp.data['remainDays']; + int remainHours = resp.data['remainHours']; + int remainMinutes = resp.data['remainMinutes']; + + if (remainDays == 0 && + remainHours == 0 && + remainMinutes == 0) { + setState(() { + isWillingToChangeMajor = true; + }); + } else { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: remainDays == 0 + ? "1일 뒤에 변경이 가능합니다." + : "$remainDays일 뒤에 변경이 가능합니다.", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); + }, + ); + }, + ); + } + } + } on DioException catch (e) { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: + e.response?.data["message"] ?? "에러 발생", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); + }, + ); + }, + ); + } catch (e) { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: "에러가 발생했습니다.", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); + }, + ); + }, + ); + } + }, + ), + if (isWillingToChangeMajor) _renderChangeMajorField(), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget _renderTop() { + return Container( + child: Stack( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + IconButton( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: const Icon( + Icons.chevron_left, + ), + ), + ], + ), + const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 12.0), + child: Text( + "내 정보", + style: TextStyle( + fontSize: 16.0, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _renderMyInfo() { + final memberState = ref.watch(memberStateNotifierProvider); + + String nickname = ""; + String universityName = ""; + String email = ""; + String major = ""; + String minor = ""; + + if (memberState is MemberModel) { + nickname = memberState.nickname; + universityName = memberState.universityName; + email = memberState.email; + major = memberState.major; + minor = memberState.minor; + } + + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + border: Border.all(color: BODY_TEXT_COLOR), + ), + padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 16.0), + height: 170.0, + child: Column( + children: [ + _renderSimpleTextBox('닉네임', nickname), + _renderSimpleTextBox('학교', universityName), + _renderSimpleTextBox('이메일', email), + _renderSimpleTextBox('전공', major), + _renderSimpleTextBox('부전공', minor), + ], + ), + ); + } + + Widget _renderSimpleTextBox(String fieldName, String content) { + return Padding( + padding: const EdgeInsets.only(bottom: 6.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + fieldName, + ), + Text( + content, + style: const TextStyle( + color: BODY_TEXT_COLOR, + ), + ), + ], + ), + ); + } + + Widget _renderButton(String text, VoidCallback onButtonClicked) { + return Padding( + padding: const EdgeInsets.only(top: 12.0), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: PRIMARY_COLOR, + minimumSize: Size( + MediaQuery.of(context).size.width, + 40.0, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + onPressed: onButtonClicked, + child: Text( + text, + style: const TextStyle( + color: Colors.white, + ), + ), + ), + ); + } + + Widget _renderChangeMajorField() { + final dio = ref.watch(dioProvider); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 12.0), + child: Column( + children: [ + _renderFirstMajorField(), + _renderSecondMajorField(), + _renderButton( + "학과 변경하기", + () async { + try { + final resp = await dio.put( + '$ip/api/belongs/change-departments', + options: Options( + headers: { + 'accessToken': 'true', + }, + ), + data: { + 'major': selectedMajor, + 'minor': selectedMinor, + }, + ); + if (resp.statusCode == 204) { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: "학과 변경이 완료되었습니다.", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); + }, + ); + }, + ); + } + } on DioException catch (e) { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: e.response?.data["message"] ?? "에러 발생", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); + }, + ); + }, + ); + } catch (e) { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: "에러가 발생했습니다.", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); + }, + ); + }, + ); + } + }, + ), + _renderButton( + "취소", + () { + setState(() { + isWillingToChangeMajor = false; + }); + }, + ), + ], + ), + ); + } + + Widget _renderFirstMajorField() { + //학과 정보 + final departmentState = ref.watch(firstDepartmentListNotifierProvider); + + String selectedDivision = departmentState.selectedDivision; + String selectedDepartment = departmentState.selectedDepartment; + + final divisionAndDepartments = departmentState.divisionAndDepartments; + + return SizedBox( + width: MediaQuery.of(context).size.width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '본전공 선택', + style: TextStyle( + color: BODY_TEXT_COLOR, + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(0, 10, 0, 10), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 240, + child: DropdownButton( + isExpanded: true, + value: selectedDivision, + items: divisionAndDepartments.keys == null + ? [] + : divisionAndDepartments.keys + .map((e) => DropdownMenuItem( + value: e, + child: Text(e), + )) + .toList(), + onChanged: (String? value) { + if (value != null && + divisionAndDepartments.containsKey(value)) { + setState(() { + selectedDivision = value; + selectedDepartment = + divisionAndDepartments[value]!.isNotEmpty + ? divisionAndDepartments[value]![0] + : null; + ref + .read( + firstDepartmentListNotifierProvider.notifier) + .setSelectedDivision(value); + ref + .read( + firstDepartmentListNotifierProvider.notifier) + .setSelectedDepartment(selectedDepartment); + setState(() { + selectedMajor = selectedDepartment; + }); + }); + } + }, + ), + ), + SizedBox( + width: 240, + child: DropdownButton( + isExpanded: true, + value: selectedDepartment, + items: divisionAndDepartments[selectedDivision] == null + ? [] + : List.from( + divisionAndDepartments[selectedDivision]) + .map((e) => DropdownMenuItem( + value: e.toString(), + child: Text(e.toString()), + )) + .toList(), + onChanged: (String? value) { + if (value != null) { + setState(() { + selectedDepartment = value; + ref + .read( + firstDepartmentListNotifierProvider.notifier) + .setSelectedDepartment(value); + setState(() { + selectedMajor = selectedDepartment; + }); + }); + } + }, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _renderSecondMajorField() { + //학과 정보 + final departmentState = ref.watch(secondDepartmentListNotifierProvider); + + String selectedDivision = departmentState.selectedDivision; + String selectedDepartment = departmentState.selectedDepartment; + + final divisionAndDepartments = departmentState.divisionAndDepartments; + + return Container( + padding: const EdgeInsets.only(top: 10.0), + width: MediaQuery.of(context).size.width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '복수전공 선택', + style: TextStyle( + color: BODY_TEXT_COLOR, + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(0, 10, 0, 10), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 240, + child: DropdownButton( + isExpanded: true, + value: selectedDivision, + items: divisionAndDepartments.keys == null + ? [] + : divisionAndDepartments.keys + .map((e) => DropdownMenuItem( + value: e, + child: Text(e), + )) + .toList(), + onChanged: (String? value) { + if (value != null && + divisionAndDepartments.containsKey(value)) { + setState(() { + selectedDivision = value; + selectedDepartment = + divisionAndDepartments[value]!.isNotEmpty + ? divisionAndDepartments[value]![0] + : null; + ref + .read( + secondDepartmentListNotifierProvider.notifier) + .setSelectedDivision(value); + ref + .read( + secondDepartmentListNotifierProvider.notifier) + .setSelectedDepartment(selectedDepartment); + setState(() { + selectedMinor = selectedDepartment; + }); + }); + } + }, + ), + ), + SizedBox( + width: 240, + child: DropdownButton( + isExpanded: true, + value: selectedDepartment, + items: divisionAndDepartments[selectedDivision] == null + ? [] + : List.from( + divisionAndDepartments[selectedDivision]) + .map((e) => DropdownMenuItem( + value: e.toString(), + child: Text(e.toString()), + )) + .toList(), + onChanged: (String? value) { + if (value != null) { + setState(() { + selectedDepartment = value; + ref + .read( + secondDepartmentListNotifierProvider.notifier) + .setSelectedDepartment(value); + setState(() { + selectedMinor = selectedDepartment; + }); + }); + } + }, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/frontend/lib/member/view/my_page_screen.dart b/frontend/lib/member/view/my_page_screen.dart new file mode 100644 index 0000000000..da7a48f9f4 --- /dev/null +++ b/frontend/lib/member/view/my_page_screen.dart @@ -0,0 +1,507 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_client_sse/flutter_client_sse.dart'; +import 'package:flutter_email_sender/flutter_email_sender.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/member/view/my_info_screen.dart'; +import 'package:frontend/member/view/my_scrap_screen.dart'; +import 'package:frontend/member/view/password_edit_screen.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +import '../../common/component/notice_popup_dialog.dart'; +import '../../common/const/colors.dart'; +import '../../common/const/data.dart'; +import '../../common/layout/default_layout.dart'; +import '../../common/provider/dio_provider.dart'; +import '../model/member_model.dart'; +import '../provider/member_state_notifier_provider.dart'; +import 'my_comment_screen.dart'; +import 'my_post_screen.dart'; + +class MyPageScreen extends ConsumerStatefulWidget { + const MyPageScreen({super.key}); + + @override + ConsumerState createState() => _MypageScreenState(); +} + +class _MypageScreenState extends ConsumerState { + void onMyInfoPressed(String email, String universityName, String nickname) { + showDialog( + context: context, + builder: (BuildContext context) { + return SimpleDialog( + title: const Text("내 정보"), + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32.0, right: 32.0, top: 10.0, bottom: 10.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + children: [ + const Text('이메일'), + const SizedBox( + width: 30.0, + ), + Text(email), + ], + ), + const SizedBox( + height: 10.0, + ), + Row( + children: [ + const Text('학교'), + const SizedBox( + width: 42.0, + ), + Text( + universityName, + overflow: TextOverflow.visible, + softWrap: true, + ), + ], + ), + const SizedBox( + height: 10.0, + ), + Row( + children: [ + const Text('닉네임'), + const SizedBox( + width: 30.0, + ), + Text(nickname), + ], + ), + ], + ), + ), + ], + ); + }, + ); + } + + void onAppInfoPressed() async { + PackageInfo info = await PackageInfo.fromPlatform(); + showDialog( + context: context, + builder: (BuildContext context) { + return SimpleDialog( + title: const Text("앱 정보"), + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32.0, right: 32.0, top: 10.0, bottom: 10.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Row( + children: [ + Text('앱 이름'), + SizedBox( + width: 30.0, + ), + Text('DeCl'), + ], + ), + const SizedBox( + height: 10.0, + ), + Row( + children: [ + const Text('앱 버전'), + const SizedBox( + width: 30.0, + ), + Text(info.version), + ], + ), + ], + ), + ), + ], + ); + }, + ); + } + + void onContactPressed() async { + //이메일은 추후 디클 전용 이메일로 변경해도 좋을듯 합니다! + final Email email = Email( + body: '문의할 사항을 아래에 작성해주세요.', + subject: '[Decl 문의]', + recipients: ['99jiasmin@gmail.com'], + cc: [], + bcc: [], + attachmentPaths: [], + isHTML: false); + + try { + await FlutterEmailSender.send(email); + } catch (error) { + String title = '문의하기'; + String message = '기본 메일 앱을 사용할 수 없습니다. \n이메일로 연락주세요! 99jiasmin@gmail.com'; + showNoticeAlert(title, message); + } + } + + void showNoticeAlert(String title, String message) { + showDialog( + context: context, + builder: (BuildContext context) { + return SimpleDialog( + title: Text( + title, + overflow: TextOverflow.visible, + softWrap: true, + ), + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32.0, right: 32.0, top: 10.0, bottom: 10.0), + child: Column( + children: [ + Text( + message, + overflow: TextOverflow.visible, + softWrap: true, + ), + ], + ), + ), + ], + ); + }, + ); + } + + void noticeBeforeLogoutDialog() async { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: "정말 로그아웃 하시겠습니까?", + buttonText: "로그아웃", + onPressed: () { + ref.read(memberStateNotifierProvider.notifier).logout(); + }, + ); + }, + ); + } + + void noticeBeforeResignDialog() async { + final dio = ref.watch(dioProvider); + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: "정말 탈퇴하시겠습니까?", + buttonText: "탈퇴하기", + onPressed: () async { + try { + final resp = await dio.post( + '$ip/api/users/resign', + options: Options( + headers: { + 'accessToken': 'true', + }, + ), + ); + if (resp.statusCode == 204) { + ref.read(memberStateNotifierProvider.notifier).logout(); + } + } on DioException catch (e) { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: e.response?.data["message"] ?? "에러가 발생했습니다.", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); + }, + ); + }, + ); + } catch (e) { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: "에러가 발생했습니다.", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); + }, + ); + }, + ); + } + + SSEClient.unsubscribeFromSSE(); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final memberState = ref.watch(memberStateNotifierProvider); + + String nickname = ""; + + if (memberState is MemberModel) { + nickname = memberState.nickname; + } + + return DefaultLayout( + child: SingleChildScrollView( + child: SafeArea( + child: Column( + children: [ + const _Top(), + _Title(nickname: nickname), + const SizedBox(height: 20.0), + _buildAccountInfo(ref, context), + const SizedBox(height: 40.0), + _buildNoticeInfo(ref, context), + ], + ), + ), + ), + ); + } + + Widget _buildAccountInfo(WidgetRef ref, BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 10), + width: MediaQuery.of(context).size.width, + child: const Text("계정 정보"), + ), + _MenuButton( + title: "내 정보", + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const MyInfoScreen(), + ), + ); + }, + border: Border( + top: BorderSide(color: Colors.grey.shade400), + bottom: BorderSide(color: Colors.grey.shade400), + left: const BorderSide(color: Colors.transparent), + right: const BorderSide(color: Colors.transparent), + ), + ), + _MenuButton( + title: "내가 쓴 글", + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const MyPostScreen(), + ), + ); + }, + ), + _MenuButton( + title: "댓글단 글", + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const MyCommentScreen(), + ), + ); + }, + ), + _MenuButton( + title: "스크랩한 글", + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const MyScrapScreen(), + ), + ); + }, + ), + _MenuButton( + title: "비밀번호 변경", + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const PasswordEditScreen(), + ), + ); + }, + ), + _MenuButton( + title: "회원 탈퇴하기", + onPressed: () { + noticeBeforeResignDialog(); + }, + ), + ], + ); + } + + Widget _buildNoticeInfo(WidgetRef ref, BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 10), + width: MediaQuery.of(context).size.width, + child: const Text("이용 안내"), + ), + _MenuButton( + title: "앱 정보", + onPressed: () { + onAppInfoPressed(); + }, + border: Border( + top: BorderSide(color: Colors.grey.shade400), + bottom: BorderSide(color: Colors.grey.shade400), + left: const BorderSide(color: Colors.transparent), + right: const BorderSide(color: Colors.transparent), + ), + ), + _MenuButton( + title: "문의하기", + onPressed: () { + onContactPressed(); + }, + ), + _MenuButton( + title: "로그아웃", + onPressed: () { + noticeBeforeLogoutDialog(); + }, + ), + ], + ); + } +} + +class _Top extends StatelessWidget { + const _Top({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(16, 10, 16, 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + IconButton( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: const Icon( + Icons.keyboard_arrow_left, + color: PRIMARY_COLOR, + ), + ), + const Icon( + Icons.home_outlined, + color: PRIMARY_COLOR, + ), + ], + ), + ); + } +} + +class _Title extends StatelessWidget { + final String nickname; + + const _Title({ + required this.nickname, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 10), + width: MediaQuery.of(context).size.width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '반가워요 $nickname님!', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ), + const Text( + '오늘도 디클에서 좋은 하루 보내세요!', + style: TextStyle( + fontSize: 16, + color: BODY_TEXT_COLOR, + ), + ), + ], + ), + ); + } +} + +class _MenuButton extends StatelessWidget { + final String title; + final VoidCallback onPressed; + final Border? border; + + const _MenuButton({ + required this.title, + required this.onPressed, + this.border, + super.key, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onPressed, + child: Container( + decoration: BoxDecoration( + border: border ?? + Border( + top: const BorderSide(color: Colors.transparent), + bottom: BorderSide(color: Colors.grey.shade400), + left: const BorderSide(color: Colors.transparent), + right: const BorderSide(color: Colors.transparent), + ), + ), + width: MediaQuery.of(context).size.width, + height: 60.0, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + const Icon( + Icons.circle, + size: 8.0, + ), + const SizedBox(width: 10.0), + Text( + title, + style: const TextStyle(fontSize: 16.0), + ), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/lib/member/view/my_post_screen.dart b/frontend/lib/member/view/my_post_screen.dart new file mode 100644 index 0000000000..98d0a83c42 --- /dev/null +++ b/frontend/lib/member/view/my_post_screen.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/member/provider/mypage/my_post_state_notifier_provider.dart'; + +import '../../board/component/board_card.dart'; +import '../../board/model/msg_board_response_model.dart'; +import '../../board/provider/board_detail_state_notifier_provider.dart'; +import '../../board/view/msg_board_screen.dart'; +import '../../common/const/colors.dart'; +import '../../common/model/cursor_pagination_model.dart'; + +class MyPostScreen extends ConsumerStatefulWidget { + const MyPostScreen({super.key}); + + @override + ConsumerState createState() => _MyPostScreenState(); +} + +class _MyPostScreenState extends ConsumerState { + final ScrollController controller = ScrollController(); + + @override + void initState() { + super.initState(); + controller.addListener(scrollListener); + } + + void scrollListener() { + if (controller.offset > controller.position.maxScrollExtent - 150) { + ref.read(myPostStateNotifierProvider.notifier).paginate( + fetchMore: true, + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Column( + children: [ + _renderTop(), + Expanded( + child: _renderMyPostList(), + ), + ], + ), + ), + ); + } + + Widget _renderTop() { + return Container( + child: Stack( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + IconButton( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: const Icon( + Icons.chevron_left, + ), + ), + ], + ), + const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 12.0), + child: Text( + "내가 쓴 글", + style: TextStyle( + fontSize: 16.0, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _renderMyPostList() { + final data = ref.watch(myPostStateNotifierProvider); + + if (data is CursorPaginationModelLoading) { + return const Center( + child: CircularProgressIndicator( + color: PRIMARY_COLOR, + ), + ); + } + + if (data is CursorPaginationModelError) { + return const Center( + child: Text("데이터를 불러올 수 없습니다."), + ); + } + + final cp = data as CursorPaginationModel; + + return RefreshIndicator( + onRefresh: () async { + ref.read(myPostStateNotifierProvider.notifier).lastId = + 9223372036854775807; + await ref + .read(myPostStateNotifierProvider.notifier) + .paginate(forceRefetch: true); + }, + child: ListView.separated( + controller: controller, + itemCount: cp.data.length + 1, + itemBuilder: (_, index) { + if (index == cp.data.length) { + return Center( + child: cp is CursorPaginationModelFetchingMore + ? const CircularProgressIndicator( + color: PRIMARY_COLOR, + ) + : const Text( + 'Copyright 2024. Decl Team all rights reserved.\n', + style: TextStyle( + color: BODY_TEXT_COLOR, + fontSize: 12.0, + ), + ), + ); + } + + final MsgBoardResponseModel pItem = cp.data[index]; + + return GestureDetector( + child: BoardCard.fromModel(msgBoardResponseModel: pItem), + onTap: () async { + // 상세페이지 + ref.read(boardDetailNotifier.notifier).add(pItem.id); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MsgBoardScreen( + board: pItem, + ), + fullscreenDialog: true), + ); + }, + ); + }, + separatorBuilder: (_, index) { + return const SizedBox(height: 1.0); + }, + ), + ); + } +} diff --git a/frontend/lib/member/view/my_scrap_screen.dart b/frontend/lib/member/view/my_scrap_screen.dart new file mode 100644 index 0000000000..440cb82d6a --- /dev/null +++ b/frontend/lib/member/view/my_scrap_screen.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/member/provider/mypage/my_scrap_state_notifier_provider.dart'; + +import '../../board/component/board_card.dart'; +import '../../board/model/msg_board_response_model.dart'; +import '../../board/provider/board_detail_state_notifier_provider.dart'; +import '../../board/view/msg_board_screen.dart'; +import '../../common/const/colors.dart'; +import '../../common/model/cursor_pagination_model.dart'; + +class MyScrapScreen extends ConsumerStatefulWidget { + const MyScrapScreen({super.key}); + + @override + ConsumerState createState() => _MyScrapScreenState(); +} + +class _MyScrapScreenState extends ConsumerState { + final ScrollController controller = ScrollController(); + + @override + void initState() { + super.initState(); + controller.addListener(scrollListener); + } + + void scrollListener() { + if (controller.offset > controller.position.maxScrollExtent - 150) { + ref.read(myScrapStateNotifierProvider.notifier).paginate( + fetchMore: true, + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Column( + children: [ + _renderTop(), + Expanded( + child: _renderMyScrappedPostList(), + ), + ], + ), + ), + ); + } + + Widget _renderTop() { + return Container( + child: Stack( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + IconButton( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: const Icon( + Icons.chevron_left, + ), + ), + ], + ), + const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 12.0), + child: Text( + "스크랩한 글", + style: TextStyle( + fontSize: 16.0, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _renderMyScrappedPostList() { + final data = ref.watch(myScrapStateNotifierProvider); + + if (data is CursorPaginationModelLoading) { + return const Center( + child: CircularProgressIndicator( + color: PRIMARY_COLOR, + ), + ); + } + + if (data is CursorPaginationModelError) { + return const Center( + child: Text("데이터를 불러올 수 없습니다."), + ); + } + + final cp = data as CursorPaginationModel; + + return RefreshIndicator( + onRefresh: () async { + ref.read(myScrapStateNotifierProvider.notifier).lastId = + 9223372036854775807; + await ref + .read(myScrapStateNotifierProvider.notifier) + .paginate(forceRefetch: true); + }, + child: ListView.separated( + controller: controller, + itemCount: cp.data.length + 1, + itemBuilder: (_, index) { + if (index == cp.data.length) { + return Center( + child: cp is CursorPaginationModelFetchingMore + ? const CircularProgressIndicator( + color: PRIMARY_COLOR, + ) + : const Text( + 'Copyright 2024. Decl Team all rights reserved.\n', + style: TextStyle( + color: BODY_TEXT_COLOR, + fontSize: 12.0, + ), + ), + ); + } + + final MsgBoardResponseModel pItem = cp.data[index]; + + return GestureDetector( + child: BoardCard.fromModel(msgBoardResponseModel: pItem), + onTap: () async { + // 상세페이지 + ref.read(boardDetailNotifier.notifier).add(pItem.id); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MsgBoardScreen( + board: pItem, + ), + fullscreenDialog: true), + ); + }, + ); + }, + separatorBuilder: (_, index) { + return const SizedBox(height: 1.0); + }, + ), + ); + } +} diff --git a/frontend/lib/member/view/password_edit_screen.dart b/frontend/lib/member/view/password_edit_screen.dart new file mode 100644 index 0000000000..68a20165ca --- /dev/null +++ b/frontend/lib/member/view/password_edit_screen.dart @@ -0,0 +1,250 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/common/layout/default_layout.dart'; + +import '../../common/component/notice_popup_dialog.dart'; +import '../../common/const/colors.dart'; +import '../../common/const/data.dart'; +import '../../common/provider/dio_provider.dart'; +import '../component/custom_text_form_field.dart'; + +class PasswordEditScreen extends ConsumerStatefulWidget { + const PasswordEditScreen({super.key}); + + @override + ConsumerState createState() => _PasswordEditScreenState(); +} + +class _PasswordEditScreenState extends ConsumerState { + String oldPassword = ""; + String password = ""; + String password2 = ""; + + bool isPasswordNull = true; + bool isPasswordDifferent = false; + + @override + Widget build(BuildContext context) { + return DefaultLayout( + child: SingleChildScrollView( + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _renderTop(), + const SizedBox(height: 10.0), + _renderPasswordField(), + const SizedBox(height: 20.0), + _renderButton(), + ], + ), + ), + ), + ), + ); + } + + Widget _renderTop() { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Stack( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + IconButton( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: const Icon( + Icons.chevron_left, + ), + ), + ], + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 10.0), + child: Center( + child: Text( + '비밀번호 변경', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _renderPasswordField() { + return Padding( + padding: const EdgeInsets.fromLTRB(0, 10, 0, 30), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('현재 비밀번호'), + const SizedBox(height: 6.0), + SizedBox( + width: MediaQuery.of(context).size.width, + child: CustomTextFormField( + isInputEnabled: true, + obscureText: true, + hintText: '현재 비밀번호를 입력해주세요.', + onChanged: (String value) { + oldPassword = value; + setState(() { + isPasswordNull = + (password == '' && password2 == '') ? true : false; + }); + }, + ), + ), + const SizedBox(height: 16.0), + const Text('새로운 비밀번호'), + const SizedBox(height: 6.0), + SizedBox( + width: MediaQuery.of(context).size.width, + child: CustomTextFormField( + isInputEnabled: true, + obscureText: true, + hintText: '새로운 비밀번호를 입력해주세요.', + onChanged: (String value) { + password = value; + setState(() { + isPasswordDifferent = password == password2 ? false : true; + isPasswordNull = + (password == '' && password2 == '') ? true : false; + }); + }, + ), + ), + const SizedBox(height: 8.0), + SizedBox( + width: MediaQuery.of(context).size.width, + child: CustomTextFormField( + isInputEnabled: true, + obscureText: true, + hintText: '새로운 비밀번호를 한번 더 입력해주세요.', + onChanged: (String value) { + password2 = value; + setState(() { + isPasswordDifferent = password == password2 ? false : true; + isPasswordNull = + (password == '' && password2 == '') ? true : false; + }); + }, + ), + ), + if (isPasswordNull) + const Padding( + padding: EdgeInsets.only(top: 8.0), + child: Text( + '비밀번호는 빈칸일 수 없습니다.', + style: TextStyle( + fontSize: 12.0, + ), + ), + ), + if (isPasswordDifferent) + const Padding( + padding: EdgeInsets.only(top: 8.0), + child: Text( + '비밀번호가 일치하지 않습니다.', + style: TextStyle( + fontSize: 12.0, + ), + ), + ), + ], + ), + ); + } + + Widget _renderButton() { + final dio = ref.watch(dioProvider); + + return ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: PRIMARY_COLOR, + textStyle: const TextStyle( + color: Colors.white, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + minimumSize: Size( + MediaQuery.of(context).size.width, + 50.0, + ), + ), + onPressed: () async { + if (!isPasswordNull && !isPasswordDifferent) { + try { + final resp = await dio.post( + '$ip/api/users/edit-password', + data: { + 'oldPassword': oldPassword, + 'password': password, + 'confirmPassword': password2, + }, + ); + if (resp.statusCode == 204) { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: "비밀번호 변경이 완료되었습니다.", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); + Navigator.pop(context); + }, + ); + }, + ); + } + } on DioException catch (e) { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: e.response?.data["message"] ?? "에러가 발생했습니다.", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); + }, + ); + }, + ); + } catch (e) { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: "에러가 발생했습니다.", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); + }, + ); + }, + ); + } + } + }, + child: const Text( + '비밀번호 변경', + style: TextStyle( + color: Colors.white, + ), + ), + ); + } +} diff --git a/frontend/lib/member/view/password_reset_screen.dart b/frontend/lib/member/view/password_reset_screen.dart new file mode 100644 index 0000000000..42a6c59c8f --- /dev/null +++ b/frontend/lib/member/view/password_reset_screen.dart @@ -0,0 +1,258 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/common/layout/default_layout.dart'; + +import '../../common/component/notice_popup_dialog.dart'; +import '../../common/const/colors.dart'; +import '../../common/const/data.dart'; +import '../../common/provider/dio_provider.dart'; +import '../component/custom_text_form_field.dart'; + +class PasswordResetScreen extends ConsumerStatefulWidget { + const PasswordResetScreen({super.key}); + + @override + ConsumerState createState() => + _PasswordResetScreenState(); +} + +class _PasswordResetScreenState extends ConsumerState { + bool isNameNull = true; + bool isEmailNull = true; + + String name = ""; + String email = ""; + + @override + Widget build(BuildContext context) { + return DefaultLayout( + child: SingleChildScrollView( + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + child: SafeArea( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _renderTop(), + const SizedBox(height: 10.0), + _renderInfo(), + const SizedBox(height: 30.0), + _renderNameField(), + _renderEmailField(), + const SizedBox(height: 20.0), + _renderButton(), + ], + ), + ), + ), + ), + ); + } + + Widget _renderTop() { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Stack( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + IconButton( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: const Icon( + Icons.chevron_left, + ), + ), + ], + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 10.0), + child: Center( + child: Text( + '비밀번호 초기화', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _renderInfo() { + return const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '비밀번호를 분실하셨나요?', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ), + Text( + '이름과 학교 이메일을 입력하시면 새로운 비밀번호를 이메일로 전송해드립니다.', + style: TextStyle( + fontSize: 14, + color: BODY_TEXT_COLOR, + ), + ), + ], + ); + } + + Widget _renderNameField() { + return Padding( + padding: const EdgeInsets.only(bottom: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('이름'), + const SizedBox(height: 6.0), + SizedBox( + width: MediaQuery.of(context).size.width, + child: CustomTextFormField( + isInputEnabled: true, + hintText: '이름(본명)을 입력해주세요.', + onChanged: (String value) { + name = value; + setState(() { + isNameNull = name == "" ? true : false; + }); + }, + ), + ), + if (isNameNull) + const Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '이름은 빈칸일 수 없습니다.', + style: TextStyle( + fontSize: 12.0, + ), + ), + ), + ], + ), + ); + } + + Widget _renderEmailField() { + return Padding( + padding: const EdgeInsets.only(bottom: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('이메일'), + const SizedBox(height: 6.0), + SizedBox( + width: MediaQuery.of(context).size.width, + child: CustomTextFormField( + isInputEnabled: true, + hintText: '이메일을 입력해주세요.', + onChanged: (String value) { + email = value; + setState(() { + isEmailNull = email == "" ? true : false; + }); + }, + ), + ), + if (isEmailNull) + const Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '이메일은 빈칸일 수 없습니다.', + style: TextStyle( + fontSize: 12.0, + ), + ), + ), + ], + ), + ); + } + + Widget _renderButton() { + final dio = ref.watch(dioProvider); + + return ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: PRIMARY_COLOR, + textStyle: const TextStyle( + color: Colors.white, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + minimumSize: Size( + MediaQuery.of(context).size.width, + 50.0, + ), + ), + onPressed: () async { + if (!isEmailNull && !isNameNull) { + try { + final resp = await dio.post( + '$ip/api/users/reset-password', + data: { + 'name': name, + 'email': email, + }, + ); + if (resp.statusCode == 204) { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: "새로운 비밀번호를 이메일로 전송했습니다.", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); + Navigator.pop(context); + }, + ); + }, + ); + } + } on DioException catch (e) { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: e.response?.data["message"] ?? "에러가 발생했습니다.", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); + }, + ); + }, + ); + } catch (e) { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: "에러가 발생했습니다.", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); + }, + ); + }, + ); + } + } + }, + child: Text('비밀번호 초기화'), + ); + } +} diff --git a/frontend/lib/member/view/signup_screen.dart b/frontend/lib/member/view/signup_screen.dart new file mode 100644 index 0000000000..b504468b4a --- /dev/null +++ b/frontend/lib/member/view/signup_screen.dart @@ -0,0 +1,954 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:frontend/common/const/data.dart'; +import 'package:frontend/common/layout/default_layout.dart'; +import 'package:frontend/common/provider/dio_provider.dart'; +import 'package:frontend/member/component/custom_text_form_field.dart'; + +import '../../common/component/notice_popup_dialog.dart'; +import '../../common/const/colors.dart'; +import '../provider/first_major_state_notifier_provider.dart'; +import '../provider/second_major_state_notifier_provider.dart'; + +class SignupScreen extends ConsumerStatefulWidget { + static String get routeName => 'signup'; + + const SignupScreen({super.key}); + + @override + ConsumerState createState() => _SignupScreenState(); +} + +class _SignupScreenState extends ConsumerState { + String name = ""; + String nickname = ""; + String email = ""; + String password = ""; + String password2 = ""; + String authNumber = ""; + String major1 = ""; + String major2 = ""; + + bool isPrivacyPolicyAccept = false; + bool isContentPolicyAccept = false; + + //이름, 닉네임 이메일, 비밀번호, 인증번호 검증 + bool isNameNull = true; + + bool isEmailNull = true; + bool isEmailAuthenticated = false; + bool isEmailSend = false; + + bool isPasswordNull = true; + bool isPasswordDifferent = false; + + bool isNicknameNull = true; + + bool isAuthNumberNull = true; + + // 복수전공 선택 여부 + bool isDoubleMajor = false; + + final String privacyPolicy = ''' + 1. 개인정보의 수집 및 이용에 대한 동의 + +가. 수집 및 이용 목적 +- 디클(DeCl)이 제공하는 커뮤니티 서비스 이용에 필요 +- 대학교 재학여부 및 본인 확인을 위하여 필요한 최소한의 범위 내에서 개인정보를 수집하고 있습니다. + +나. 수집 및 이용 항목 +- 필수항목 : 성명, 닉네임, 전자우편, 학과(1전공) +- 선택항목 : 학과(2전공) + +다. 개인정보의 보유 및 이용 기간 +- 이용자의 개인정보 수집ᆞ이용에 관한 동의일로부터 채용절차 종료 시까지 위 이용목적을 위하여 보유 및 이용하게 됩니다. 단, 서비스 종료 후에는 분쟁 해결 및 법령상 의무이행 등을 위하여 1년간 보유하게 됩니다. + +라. 동의를 거부할 권리 및 동의를 거부할 경우의 불이익 +- 위 개인정보 중 필수정보의 수집ᆞ이용에 관한 동의는 서비스 이용을 위해 필수적이므로, 위 사항에 동의하셔야만 서비스의 이용이 가능합니다. +- 지원자는 개인정보의 선택항목 제공 동의를 거부할 권리가 있습니다. 다만, 지원자가 선택항목 동의를 거부하는 경우 원활한 정보 확인을 할 수 없어 서비스 이용에 제한받을 수 있습니다. + +2. 민감정보 수집에 대한 동의 (민감정보 기재 시에만 한함) +가. 해당 사항 없음 + +3. 개인정보의 제3자 제공에 대한 동의 +가. 해당사항없음 + +나는 디클(DeCl)이 위와 같이 개인정보를 수집ᆞ이용하는 것에 동의합니다. + '''; + + final String contentPolicy = ''' + 디클은 안전하고 즐거운 커뮤니티 운영을 위해 커뮤니티 운영 규칙을 제정하여 운영하고 있습니다. + + 불법, 도박, 음란물, 도배, 욕설, 자살 관련 표현 등 사용자들에게 불쾌감을 줄 수 있는 부적절한 모든 컨텐츠(글, 댓글, 사진 등)를 생성하지 않도록 유의해주세요. + + 위반 시 게시글이 삭제되고 서비스 이용이 일정기간 제한될 수 있으며, 관련된 법적 문제 발생시 철저한 불관용 원칙을 적용합니다. + + 나는 디클(DeCl)이 위와 같이 커뮤니티 운영 규칙을 적용하는 것에 대해 동의합니다. + '''; + + @override + Widget build(BuildContext context) { + return DefaultLayout( + child: SingleChildScrollView( + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + child: SafeArea( + top: true, + bottom: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 10, 16, 40), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _renderTop(), + const SizedBox(height: 20.0), + Column( + children: [ + _renderNameField(), + _renderNicknameField(), + _renderEmailField(), + _renderFirstMajorField(), + if (!isDoubleMajor) _renderAddMajorButton(), + if (isDoubleMajor) _renderSecondMajorField(), + if (isDoubleMajor) _renderDeleteMajorButton(), + _renderPasswordField(), + _renderIsAcceptCheckbox(), + _renderRegisterButton(), + ], + ), + ], + ), + ), + ), + ), + ); + } + + Widget _renderTop() { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Stack( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + IconButton( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: const Icon( + Icons.chevron_left, + color: PRIMARY_COLOR, + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: Center( + child: Image.asset( + 'asset/imgs/logo.png', + width: 80.0, + ), + ), + ), + ], + ), + ); + } + + Widget _renderNameField() { + return Padding( + padding: const EdgeInsets.only(bottom: 30.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('이름'), + const SizedBox(height: 6.0), + SizedBox( + width: MediaQuery.of(context).size.width, + child: CustomTextFormField( + isInputEnabled: true, + hintText: '이름(본명)을 입력해주세요.', + onChanged: (String value) { + name = value; + setState(() { + isNameNull = name == "" ? true : false; + }); + }, + ), + ), + if (isNameNull) + const Padding( + padding: EdgeInsets.only(top: 8.0), + child: Text( + '이름은 빈칸일 수 없습니다.', + style: TextStyle( + fontSize: 12.0, + ), + ), + ), + ], + ), + ); + } + + Widget _renderNicknameField() { + return Padding( + padding: const EdgeInsets.only(bottom: 30.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('닉네임'), + const SizedBox(height: 6.0), + SizedBox( + width: MediaQuery.of(context).size.width, + child: CustomTextFormField( + isInputEnabled: true, + hintText: '닉네임을 입력해주세요.', + onChanged: (String value) { + nickname = value; + setState(() { + isNicknameNull = nickname == "" ? true : false; + }); + }, + ), + ), + if (isNicknameNull) + const Padding( + padding: EdgeInsets.only(top: 8.0), + child: Text( + '닉네임은 빈칸일 수 없습니다.', + style: TextStyle( + fontSize: 12.0, + ), + ), + ), + ], + ), + ); + } + + Widget _renderEmailField() { + final dio = ref.watch(dioProvider); + + return Padding( + padding: const EdgeInsets.only(bottom: 30.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('이메일'), + const SizedBox(height: 6.0), + Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width / 3 * 2, + child: CustomTextFormField( + isInputEnabled: !isEmailSend, + hintText: '대학교 이메일을 입력해주세요.', + onChanged: (String value) { + email = value; + setState(() { + isEmailNull = email == "" ? true : false; + }); + }, + ), + ), + const SizedBox(width: 12.0), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: PRIMARY_COLOR, + textStyle: const TextStyle( + color: Colors.white, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + onPressed: (isEmailNull || isEmailAuthenticated) + ? null + : () async { + try { + final resp = await dio.post( + '$ip/api/users/authentication-code?email=$email', + ); + if (resp.statusCode == 204) { + setState(() { + isEmailSend = true; + }); + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: "인증번호가 전송되었습니다.", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); // 두 번째 팝업 닫기 + }, + ); + }, + ); + } + } on DioException catch (e) { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: e.response?.data["message"] ?? "에러 발생", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); // 두 번째 팝업 닫기 + }, + ); + }, + ); + } + }, + child: const Text( + '전송', + style: TextStyle(color: Colors.white), + ), + ), + ], + ), + if (isEmailNull) + const Padding( + padding: EdgeInsets.only(top: 8.0), + child: Text( + '이메일은 빈칸일 수 없습니다.', + style: TextStyle( + fontSize: 12.0, + ), + ), + ), + const SizedBox(height: 16.0), + Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width / 3 * 2, + child: CustomTextFormField( + isInputEnabled: !isEmailAuthenticated, + hintText: '인증번호를 입력해주세요.', + onChanged: (String value) { + authNumber = value; + setState(() { + isAuthNumberNull = authNumber == "" ? true : false; + }); + }, + ), + ), + const SizedBox(width: 12.0), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: PRIMARY_COLOR, + textStyle: const TextStyle( + color: Colors.white, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + onPressed: (isEmailNull || isEmailAuthenticated) + ? null + : () async { + try { + final resp = await dio.post( + '$ip/api/users/authenticate-email?email=$email&authenticationCode=$authNumber', + ); + if (resp.statusCode == 204) { + setState(() { + isEmailAuthenticated = true; + }); + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: "인증이 완료되었습니다.", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); // 두 번째 팝업 닫기 + }, + ); + }, + ); + } + } on DioException catch (e) { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: e.response?.data["message"] ?? "에러 발생", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); // 두 번째 팝업 닫기 + }, + ); + }, + ); + } + }, + child: const Text( + '확인', + style: TextStyle(color: Colors.white), + ), + ), + ], + ), + if (isAuthNumberNull) + const Padding( + padding: EdgeInsets.only(top: 8.0), + child: Text( + '인증번호는 빈칸일 수 없습니다.', + style: TextStyle( + fontSize: 12.0, + ), + ), + ), + ], + ), + ); + } + + Widget _renderFirstMajorField() { + //학과 정보 + final departmentState = ref.watch(firstDepartmentListNotifierProvider); + + String selectedDivision = departmentState.selectedDivision; + String selectedDepartment = departmentState.selectedDepartment; + + final divisionAndDepartments = departmentState.divisionAndDepartments; + + return SizedBox( + width: MediaQuery.of(context).size.width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '본전공 선택', + ), + Padding( + padding: const EdgeInsets.fromLTRB(0, 10, 0, 10), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 180, + child: DropdownButton( + isExpanded: true, + value: selectedDivision, + items: divisionAndDepartments.keys == null + ? [] + : divisionAndDepartments.keys + .map((e) => DropdownMenuItem( + value: e, + child: Text(e), + )) + .toList(), + onChanged: (String? value) { + if (value != null && + divisionAndDepartments.containsKey(value)) { + setState(() { + selectedDivision = value; + selectedDepartment = + divisionAndDepartments[value]!.isNotEmpty + ? divisionAndDepartments[value]![0] + : null; + ref + .read( + firstDepartmentListNotifierProvider.notifier) + .setSelectedDivision(value); + ref + .read( + firstDepartmentListNotifierProvider.notifier) + .setSelectedDepartment(selectedDepartment); + setState(() { + major1 = selectedDepartment; + }); + }); + } + }, + ), + ), + SizedBox( + width: 180, + child: DropdownButton( + isExpanded: true, + value: selectedDepartment, + items: divisionAndDepartments[selectedDivision] == null + ? [] + : List.from( + divisionAndDepartments[selectedDivision]) + .map((e) => DropdownMenuItem( + value: e.toString(), + child: Text(e.toString()), + )) + .toList(), + onChanged: (String? value) { + if (value != null) { + setState(() { + selectedDepartment = value; + ref + .read( + firstDepartmentListNotifierProvider.notifier) + .setSelectedDepartment(value); + setState(() { + major1 = selectedDepartment; + }); + }); + } + }, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _renderSecondMajorField() { + //학과 정보 + final departmentState = ref.watch(secondDepartmentListNotifierProvider); + + String selectedDivision = departmentState.selectedDivision; + String selectedDepartment = departmentState.selectedDepartment; + + final divisionAndDepartments = departmentState.divisionAndDepartments; + + return Container( + padding: const EdgeInsets.only(top: 10.0), + width: MediaQuery.of(context).size.width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '복수전공 선택', + ), + Padding( + padding: const EdgeInsets.fromLTRB(0, 10, 0, 10), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 180, + child: DropdownButton( + isExpanded: true, + value: selectedDivision, + items: divisionAndDepartments.keys == null + ? [] + : divisionAndDepartments.keys + .map((e) => DropdownMenuItem( + value: e, + child: Text(e), + )) + .toList(), + onChanged: (String? value) { + if (value != null && + divisionAndDepartments.containsKey(value)) { + setState(() { + selectedDivision = value; + selectedDepartment = + divisionAndDepartments[value]!.isNotEmpty + ? divisionAndDepartments[value]![0] + : null; + ref + .read( + secondDepartmentListNotifierProvider.notifier) + .setSelectedDivision(value); + ref + .read( + secondDepartmentListNotifierProvider.notifier) + .setSelectedDepartment(selectedDepartment); + setState(() { + major2 = selectedDepartment; + }); + }); + } + }, + ), + ), + SizedBox( + width: 180, + child: DropdownButton( + isExpanded: true, + value: selectedDepartment, + items: divisionAndDepartments[selectedDivision] == null + ? [] + : List.from( + divisionAndDepartments[selectedDivision]) + .map((e) => DropdownMenuItem( + value: e.toString(), + child: Text(e.toString()), + )) + .toList(), + onChanged: (String? value) { + if (value != null) { + setState(() { + selectedDepartment = value; + ref + .read( + secondDepartmentListNotifierProvider.notifier) + .setSelectedDepartment(value); + setState(() { + major2 = selectedDepartment; + }); + }); + } + }, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _renderPasswordField() { + return Padding( + padding: const EdgeInsets.fromLTRB(0, 10, 0, 30), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width, + child: CustomTextFormField( + isInputEnabled: true, + obscureText: true, + hintText: '비밀번호를 입력해주세요.', + onChanged: (String value) { + password = value; + setState(() { + isPasswordDifferent = password == password2 ? false : true; + isPasswordNull = + (password == '' && password2 == '') ? true : false; + }); + }, + ), + ), + const SizedBox(height: 8.0), + SizedBox( + width: MediaQuery.of(context).size.width, + child: CustomTextFormField( + isInputEnabled: true, + obscureText: true, + hintText: '비밀번호를 한번 더 입력해주세요.', + onChanged: (String value) { + password2 = value; + setState(() { + isPasswordDifferent = password == password2 ? false : true; + isPasswordNull = + (password == '' && password2 == '') ? true : false; + }); + }, + ), + ), + if (isPasswordNull) + const Padding( + padding: EdgeInsets.only(top: 8.0), + child: Text( + '비밀번호는 빈칸일 수 없습니다.', + style: TextStyle( + fontSize: 12.0, + ), + ), + ), + if (isPasswordDifferent) + const Padding( + padding: EdgeInsets.only(top: 8.0), + child: Text( + '비밀번호가 일치하지 않습니다.', + style: TextStyle( + fontSize: 12.0, + ), + ), + ), + ], + ), + ); + } + + Widget _renderIsAcceptCheckbox() { + return Padding( + padding: const EdgeInsets.only(bottom: 30.0), + child: Column( + children: [ + Row( + children: [ + const Text('개인정보 수집 및 이용 동의'), + Checkbox( + activeColor: PRIMARY_COLOR, + value: isPrivacyPolicyAccept, + onChanged: (bool? value) { + setState(() { + isPrivacyPolicyAccept = value ?? false; + }); + }, + ), + TextButton( + style: ElevatedButton.styleFrom( + foregroundColor: PRIMARY_COLOR, + minimumSize: const Size(80.0, 30.0), + ), + onPressed: () { + showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return AlertDialog( + content: SingleChildScrollView( + child: Text( + privacyPolicy, + overflow: TextOverflow.visible, + style: const TextStyle( + fontSize: 12.0, + ), + softWrap: true, + ), + ), + actions: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: PRIMARY_COLOR, + textStyle: const TextStyle( + color: Colors.white, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Center( + child: Text( + '닫기', + style: TextStyle(color: Colors.white), + ), + ), + ), + ], + ); + }, + ); + }, + child: const Text( + '내용 확인', + ), + ), + ], + ), + Row( + children: [ + const Text('콘텐츠 생성 및 이용 관련 동의'), + Checkbox( + activeColor: PRIMARY_COLOR, + value: isContentPolicyAccept, + onChanged: (bool? value) { + setState(() { + isContentPolicyAccept = value ?? false; + }); + }, + ), + TextButton( + style: ElevatedButton.styleFrom( + foregroundColor: PRIMARY_COLOR, + minimumSize: const Size(80.0, 30.0), + ), + onPressed: () { + showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return AlertDialog( + content: SingleChildScrollView( + child: Text( + contentPolicy, + overflow: TextOverflow.visible, + style: const TextStyle( + fontSize: 12.0, + ), + softWrap: true, + ), + ), + actions: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: PRIMARY_COLOR, + textStyle: const TextStyle( + color: Colors.white, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Center( + child: Text( + '닫기', + style: TextStyle(color: Colors.white), + ), + ), + ), + ], + ); + }, + ); + }, + child: const Text('내용 확인')), + ], + ), + ], + ), + ); + } + + Widget _renderRegisterButton() { + final dio = ref.watch(dioProvider); + + return ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: PRIMARY_COLOR, + textStyle: const TextStyle( + color: Colors.white, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + minimumSize: Size( + MediaQuery.of(context).size.width, + 50.0, + ), + ), + onPressed: () async { + if (!isEmailNull && + isEmailAuthenticated && + !isPasswordNull && + !isPasswordDifferent && + !isNicknameNull && + !isNameNull && + isPrivacyPolicyAccept && + isContentPolicyAccept + ) { + try { + final resp = await dio.post( + '$ip/api/users/register', + data: { + 'name': name, + 'email': email, + 'nickname': nickname, + 'password': password, + 'confirmPassword': password2, + 'authenticationCode': authNumber, + 'major': major1, + 'minor': major2, + }, + ); + if (resp.statusCode == 200) { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: "회원가입이 완료되었습니다.", + buttonText: "닫기", + onPressed: () { + //Dialog를 닫고 로그인페이지로 나가야 하므로 두번 pop. + Navigator.pop(context); + Navigator.pop(context); + }, + ); + }, + ); + } + } on DioException catch (e) { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: e.response?.data["message"] ?? "에러 발생", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); + }, + ); + }, + ); + } catch (e) { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: "알 수 없는 에러가 발생했습니다.", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); + }, + ); + }, + ); + } + } else { + showDialog( + context: context, + builder: (context) { + return NoticePopupDialog( + message: "필요한 정보를 모두 입력해주세요.", + buttonText: "닫기", + onPressed: () { + Navigator.pop(context); + }, + ); + }, + ); + } + }, + child: const Text( + '회원가입하기', + style: TextStyle(color: Colors.white), + ), + ); + } + + Widget _renderAddMajorButton() { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + TextButton.icon( + onPressed: () { + setState(() { + isDoubleMajor = true; + major2 = ref + .read(secondDepartmentListNotifierProvider.notifier) + .state + .selectedDepartment; + }); + }, + icon: const Icon(Icons.add), + label: const Text('복수전공 추가'), + ), + ], + ); + } + + Widget _renderDeleteMajorButton() { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + TextButton.icon( + onPressed: () { + setState(() { + isDoubleMajor = false; + major2 = ""; + }); + }, + icon: const Icon(Icons.clear), + label: const Text('복수전공 취소'), + ), + ], + ); + } +} diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock new file mode 100644 index 0000000000..04453f2b27 --- /dev/null +++ b/frontend/pubspec.lock @@ -0,0 +1,1202 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + url: "https://pub.dev" + source: hosted + version: "64.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + app_version_update: + dependency: "direct main" + description: + name: app_version_update + sha256: "08f3c2583947a7f90cc0d8494d624dc771e22ecbc5dd52fa8068a2d0a62b8f95" + url: "https://pub.dev" + source: hosted + version: "4.0.1" + args: + dependency: transitive + description: + name: args + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + aws_cloudwatch: + dependency: "direct main" + description: + name: aws_cloudwatch + sha256: "3d68b568163321e030297dede6ddadd6db7ab9cf5a3a416fba73dfe2468f948b" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + aws_request: + dependency: transitive + description: + name: aws_request + sha256: "5cd82d75bb85dafa8234175932e7ce4d0745ea35c05d37f3fdb270102c839092" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + url: "https://pub.dev" + source: hosted + version: "4.0.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" + url: "https://pub.dev" + source: hosted + version: "2.4.9" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + url: "https://pub.dev" + source: hosted + version: "7.3.0" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + url: "https://pub.dev" + source: hosted + version: "8.9.2" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + url: "https://pub.dev" + source: hosted + version: "4.10.0" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + url: "https://pub.dev" + source: hosted + version: "0.3.3+8" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + url: "https://pub.dev" + source: hosted + version: "2.3.6" + dbus: + dependency: transitive + description: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" + dio: + dependency: "direct main" + description: + name: dio + sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5" + url: "https://pub.dev" + source: hosted + version: "5.4.3+1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385 + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_client_sse: + dependency: "direct main" + description: + name: flutter_client_sse + sha256: "2204f182dd22b859f98e30572fa8318176556eba4da8513c3bbc0632f2b79b8c" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_email_sender: + dependency: "direct main" + description: + name: flutter_email_sender + sha256: fb515d4e073d238d0daf1d765e5318487b6396d46b96e0ae9745dbc9a133f97a + url: "https://pub.dev" + source: hosted + version: "6.0.3" + flutter_keyboard_visibility: + dependency: "direct main" + description: + name: flutter_keyboard_visibility + sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_keyboard_visibility_linux: + dependency: transitive + description: + name: flutter_keyboard_visibility_linux + sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_macos: + dependency: transitive + description: + name: flutter_keyboard_visibility_macos + sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_platform_interface: + dependency: transitive + description: + name: flutter_keyboard_visibility_platform_interface + sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_web: + dependency: transitive + description: + name: flutter_keyboard_visibility_web + sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_windows: + dependency: transitive + description: + name: flutter_keyboard_visibility_windows + sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "40e6fbd2da7dcc7ed78432c5cdab1559674b4af035fddbfb2f9a8f9c2112fcef" + url: "https://pub.dev" + source: hosted + version: "17.1.2" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" + url: "https://pub.dev" + source: hosted + version: "4.0.0+1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "340abf67df238f7f0ef58f4a26d2a83e1ab74c77ab03cd2b2d5018ac64db30b7" + url: "https://pub.dev" + source: hosted + version: "7.1.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" + url: "https://pub.dev" + source: hosted + version: "2.0.19" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0" + url: "https://pub.dev" + source: hosted + version: "9.2.2" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: "00d1b67d6e9fa443331da229084dd3eb04407f5a2dff22940bd7bba6af5722c3" + url: "https://pub.dev" + source: hosted + version: "7.1.1" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + http: + dependency: "direct main" + description: + name: http + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + url: "https://pub.dev" + source: hosted + version: "1.2.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "1f498d086203360cca099d20ffea2963f48c39ce91bdd8a3b6d4a045786b02c8" + url: "https://pub.dev" + source: hosted + version: "1.0.8" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "844c6da4e4f2829dffdab97816bca09d0e0977e8dcef7450864aba4e07967a58" + url: "https://pub.dev" + source: hosted + version: "0.8.9+6" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: e2423c53a68b579a7c37a1eda967b8ae536c3d98518e5db95ca1fe5719a730a3 + url: "https://pub.dev" + source: hosted + version: "3.0.2" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "917a5cadd67d052554cfb258595e54217de53fac5b52939426e26319a02e6297" + url: "https://pub.dev" + source: hosted + version: "0.8.9+2" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b + url: "https://pub.dev" + source: hosted + version: "2.9.3" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + url: "https://pub.dev" + source: hosted + version: "6.8.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + logger: + dependency: "direct main" + description: + name: logger + sha256: af05cc8714f356fd1f3888fb6741cbe9fbe25cdb6eedbab80e1a6db21047d4a4 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + meta: + dependency: transitive + description: + name: meta + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + mime: + dependency: transitive + description: + name: mime + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + url: "https://pub.dev" + source: hosted + version: "2.2.4" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" + url: "https://pub.dev" + source: hosted + version: "11.3.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "8bb852cd759488893805c3161d0b2b5db55db52f773dbb014420b304055ba2c5" + url: "https://pub.dev" + source: hosted + version: "12.0.6" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662 + url: "https://pub.dev" + source: hosted + version: "9.4.4" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" + url: "https://pub.dev" + source: hosted + version: "0.1.1" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20" + url: "https://pub.dev" + source: hosted + version: "4.2.1" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + photo_view: + dependency: "direct main" + description: + name: photo_view + sha256: "8036802a00bae2a78fc197af8a158e3e2f7b500561ed23b4c458107685e645bb" + url: "https://pub.dev" + source: hosted + version: "0.14.0" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + retrofit: + dependency: "direct main" + description: + name: retrofit + sha256: "13a2865c0d97da580ea4e3c64d412d81f365fd5b26be2a18fca9582e021da37a" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + retrofit_generator: + dependency: "direct dev" + description: + name: retrofit_generator + sha256: "9499eb46b3657a62192ddbc208ff7e6c6b768b19e83c1ee6f6b119c864b99690" + url: "https://pub.dev" + source: hosted + version: "7.0.8" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d + url: "https://pub.dev" + source: hosted + version: "2.5.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + smooth_page_indicator: + dependency: "direct main" + description: + name: smooth_page_indicator + sha256: "725bc638d5e79df0c84658e1291449996943f93bacbc2cec49963dbbab48d8ae" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + url: "https://pub.dev" + source: hosted + version: "1.3.4" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + timezone: + dependency: transitive + description: + name: timezone + sha256: a6ccda4a69a442098b602c44e61a1e2b4bf6f5516e875bbf0f427d5df14745d5 + url: "https://pub.dev" + source: hosted + version: "0.9.3" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" + url: "https://pub.dev" + source: hosted + version: "6.2.6" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9" + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" + url: "https://pub.dev" + source: hosted + version: "6.3.0" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + url: "https://pub.dev" + source: hosted + version: "2.2.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + url: "https://pub.dev" + source: hosted + version: "3.1.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.dev" + source: hosted + version: "0.3.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + win32: + dependency: transitive + description: + name: win32 + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + url: "https://pub.dev" + source: hosted + version: "5.2.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.2.3 <3.7.12" + flutter: ">=3.16.6" diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml new file mode 100644 index 0000000000..43c46fbc46 --- /dev/null +++ b/frontend/pubspec.yaml @@ -0,0 +1,135 @@ +name: frontend +description: 2024 capstone +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: "none" # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.7+8 + +environment: + sdk: ">=3.1.0 <4.0.0" + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + dio: ^5.4.0 + flutter_secure_storage: ^9.0.0 + json_annotation: ^4.8.1 + retrofit: ">=4.0.0 <5.0.0" + logger: any + flutter_riverpod: ^2.4.10 + go_router: ^7.0.1 + image_picker: ^1.0.7 + flutter_email_sender: ^6.0.3 + package_info_plus: ^4.2.0 + http: ^1.1.0 + photo_view: ^0.14.0 + flutter_client_sse: ^2.0.1 + flutter_local_notifications: ^17.0.0 + smooth_page_indicator: ^1.1.0 + flutter_keyboard_visibility: ^6.0.0 + crypto: ^3.0.3 + permission_handler: ^11.3.1 + aws_cloudwatch: ^1.0.0 + intl: ^0.18.0 + app_version_update: ^4.0.1 + url_launcher: ^6.0.20 + +dev_dependencies: + build_runner: ^2.3.3 + json_serializable: ^6.6.0 + retrofit_generator: ">=7.0.0 <8.0.0" + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + assets: + - asset/imgs/decle.png + - asset/imgs/logo.png + - asset/imgs/logo_white.png + - asset/jsons/departments.json + + fonts: + - family: NotoSans + fonts: + - asset: asset/fonts/NotoSansKR-Black.otf + weight: 900 + - asset: asset/fonts/NotoSansKR-Bold.otf + weight: 700 + - asset: asset/fonts/NotoSansKR-Medium.otf + weight: 500 + - asset: asset/fonts/NotoSansKR-Regular.otf + weight: 400 + - asset: asset/fonts/NotoSansKR-Light.otf + weight: 300 + - asset: asset/fonts/NotoSansKR-Thin.otf + weight: 100 + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/frontend/test/widget_test.dart b/frontend/test/widget_test.dart new file mode 100644 index 0000000000..bf429d482d --- /dev/null +++ b/frontend/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:frontend/main.dart'; + +// void main() { +// testWidgets('Counter increments smoke test', (WidgetTester tester) async { +// // Build our app and trigger a frame. +// await tester.pumpWidget(const MyApp()); +// +// // Verify that our counter starts at 0. +// expect(find.text('0'), findsOneWidget); +// expect(find.text('1'), findsNothing); +// +// // Tap the '+' icon and trigger a frame. +// await tester.tap(find.byIcon(Icons.add)); +// await tester.pump(); +// +// // Verify that our counter has incremented. +// expect(find.text('0'), findsNothing); +// expect(find.text('1'), findsOneWidget); +// }); +// } diff --git a/index.md b/index.md deleted file mode 100644 index b1e80ac9bb..0000000000 --- a/index.md +++ /dev/null @@ -1,37 +0,0 @@ -## Welcome to GitHub Pages - -You can use the [editor on GitHub](https://github.com/kookmin-sw/cap-template/edit/master/index.md) to maintain and preview the content for your website in Markdown files. - -Whenever you commit to this repository, GitHub Pages will run [Jekyll](https://jekyllrb.com/) to rebuild the pages in your site, from the content in your Markdown files. - -### Markdown - -Markdown is a lightweight and easy-to-use syntax for styling your writing. It includes conventions for - -```markdown -Syntax highlighted code block - -# Header 1 -## Header 2 -### Header 3 - -- Bulleted -- List - -1. Numbered -2. List - -**Bold** and _Italic_ and `Code` text - -[Link](url) and ![Image](src) -``` - -For more details see [GitHub Flavored Markdown](https://guides.github.com/features/mastering-markdown/). - -### Jekyll Themes - -Your Pages site will use the layout and styles from the Jekyll theme you have selected in your [repository settings](https://github.com/kookmin-sw/cap-template/settings). The name of this theme is saved in the Jekyll `_config.yml` configuration file. - -### Support or Contact - -Having trouble with Pages? Check out our [documentation](https://help.github.com/categories/github-pages-basics/) or [contact support](https://github.com/contact) and we’ll help you sort it out. diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 0000000000..11b02abd48 --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1,36 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +variables.tf \ No newline at end of file diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl new file mode 100644 index 0000000000..0b1a4ee23f --- /dev/null +++ b/terraform/.terraform.lock.hcl @@ -0,0 +1,32 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "4.67.0" + constraints = "~> 4.0" + hashes = [ + "h1:5Zfo3GfRSWBaXs4TGQNOflr1XaYj6pRnVJLX5VAjFX4=", + "zh:0843017ecc24385f2b45f2c5fce79dc25b258e50d516877b3affee3bef34f060", + "zh:19876066cfa60de91834ec569a6448dab8c2518b8a71b5ca870b2444febddac6", + "zh:24995686b2ad88c1ffaa242e36eee791fc6070e6144f418048c4ce24d0ba5183", + "zh:4a002990b9f4d6d225d82cb2fb8805789ffef791999ee5d9cb1fef579aeff8f1", + "zh:559a2b5ace06b878c6de3ecf19b94fbae3512562f7a51e930674b16c2f606e29", + "zh:6a07da13b86b9753b95d4d8218f6dae874cf34699bca1470d6effbb4dee7f4b7", + "zh:768b3bfd126c3b77dc975c7c0e5db3207e4f9997cf41aa3385c63206242ba043", + "zh:7be5177e698d4b547083cc738b977742d70ed68487ce6f49ecd0c94dbf9d1362", + "zh:8b562a818915fb0d85959257095251a05c76f3467caa3ba95c583ba5fe043f9b", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9c385d03a958b54e2afd5279cd8c7cbdd2d6ca5c7d6a333e61092331f38af7cf", + "zh:b3ca45f2821a89af417787df8289cb4314b273d29555ad3b2a5ab98bb4816b3b", + "zh:da3c317f1db2469615ab40aa6baba63b5643bae7110ff855277a1fb9d8eb4f2c", + "zh:dc6430622a8dc5cdab359a8704aec81d3825ea1d305bbb3bbd032b1c6adfae0c", + "zh:fac0d2ddeadf9ec53da87922f666e1e73a603a611c57bcbc4b86ac2821619b1d", + ] +} + +provider "registry.terraform.io/hashicorp/template" { + version = "2.2.0" + hashes = [ + "h1:oXKLw3Ha3+kLGfdUWVZkp9Yg595d4E3HQ3jvnjZjca4=", + ] +} diff --git a/terraform/acm.tf b/terraform/acm.tf new file mode 100644 index 0000000000..17e1be7961 --- /dev/null +++ b/terraform/acm.tf @@ -0,0 +1,12 @@ +resource "aws_acm_certificate" "cert" { + domain_name = var.domain + validation_method = "DNS" + lifecycle { + create_before_destroy = true + } +} + +resource "aws_acm_certificate_validation" "cert" { + certificate_arn = aws_acm_certificate.cert.arn + validation_record_fqdns = [aws_route53_record.cert_validation.fqdn] +} \ No newline at end of file diff --git a/terraform/autoscaling.tf b/terraform/autoscaling.tf new file mode 100644 index 0000000000..9f701bee3b --- /dev/null +++ b/terraform/autoscaling.tf @@ -0,0 +1,39 @@ +resource "aws_appautoscaling_target" "ecs_target" { + max_capacity = var.scaling_max_capacity + min_capacity = var.scaling_min_capacity + resource_id = "service/${aws_ecs_cluster.staging.name}/${aws_ecs_service.staging.name}" + scalable_dimension = "ecs:service:DesiredCount" + service_namespace = "ecs" +} + +resource "aws_appautoscaling_policy" "ecs_policy_memory" { + name = "memory-autoscaling" + policy_type = "TargetTrackingScaling" + resource_id = aws_appautoscaling_target.ecs_target.resource_id + scalable_dimension = aws_appautoscaling_target.ecs_target.scalable_dimension + service_namespace = aws_appautoscaling_target.ecs_target.service_namespace + + target_tracking_scaling_policy_configuration { + predefined_metric_specification { + predefined_metric_type = "ECSServiceAverageMemoryUtilization" + } + + target_value = var.cpu_or_memory_limit + } +} + +resource "aws_appautoscaling_policy" "ecs_policy_cpu" { + name = "cpu-autoscaling" + policy_type = "TargetTrackingScaling" + resource_id = aws_appautoscaling_target.ecs_target.resource_id + scalable_dimension = aws_appautoscaling_target.ecs_target.scalable_dimension + service_namespace = aws_appautoscaling_target.ecs_target.service_namespace + + target_tracking_scaling_policy_configuration { + predefined_metric_specification { + predefined_metric_type = "ECSServiceAverageCPUUtilization" + } + + target_value = var.cpu_or_memory_limit + } +} \ No newline at end of file diff --git a/terraform/ecr.tf b/terraform/ecr.tf new file mode 100644 index 0000000000..e9505a0618 --- /dev/null +++ b/terraform/ecr.tf @@ -0,0 +1,90 @@ +resource "aws_ecr_repository" "repo" { + name = "dclass/service_${var.env_suffix}" + image_tag_mutability = "MUTABLE" + + image_scanning_configuration { + scan_on_push = false + } +} + +resource "aws_ecr_lifecycle_policy" "repo-policy" { + repository = aws_ecr_repository.repo.name + + policy = <