Web 2024. 4. 23. 13:26

접속할 때마다 열받는 사이트 중 하나... 뭔가 안 된다 싶으면 바로 지원센터로 전화할 것

 

1. Windows + R 키를 눌러서 실행 창을 띄우고 %localappdata% 입력 (혹은 파일탐색기에서 C:\Users\유저명\AppData\Local 으로 접근)

2. 파일탐색기가 열리면 court 폴더 내의 파일 2개 삭제

3. 인터넷 등기소에서 발급버튼 클릭 후 테스트 출력 진행

4. 발급 버튼을 한 번 더 클릭

 

 

'Web' 카테고리의 다른 글

구글 검색으로 Tistory 접속 시 400 에러 발생할 때  (0) 2023.04.22
티스토리 마크다운 CSS 적용  (0) 2021.02.21
Web 2023. 4. 22. 23:20

구글 검색으로 Tistory 접속 시 400 에러 발생할 때

  • 브라우저 캐시 문제였다.
  • 에러 페이지에서 F12로 개발자 도구를 열고, Application 탭의 Storage > Cookies 에서 tistory.com 사이트를 우클릭하여 Clear하면 해결된다.

Web/React 2021. 8. 18. 23:58

리액트 gh-pages 적용

  • 리액트로 만든 사이트를 github에서 호스팅할 경우, gh-pages 패키지를 사용하면 편하다.

  • 일반적인 깃허브 페이지는 빌드한 파일들을 해당 브랜치에 넣어두는데, 이 패키지를 사용하면 원격 리포지토리에 gh-pages라는 브랜치가 생기고, 거기에 빌드하게 된다.


적용 과정

  • 다음 명령어 중 하나로 gh-pages 패키지를 설치한다.

    $ npm install gh-pages
    $ yarn add gh-pages
  • 프로젝트의 package.json 파일을 아래처럼 수정한다.

    {
      "homepage": "도메인주소",
      ...(생략),
      "scripts": {
        ...(생략),
        "predeploy": "npm run build",
        "deploy": "gh-pages -d build"
      }
    }
  • 이 후 배포할 때 다음 명령어 중 하나를 실행하면 새로운 브랜치로 pull request를 할 수 있고, 이를 처리하면 gh-pages 브랜치가 생성된다.

    $ npm run deploy
    $ yarn deploy
  • 이후 깃허브의 Settings -> Pages 에서 브랜치를 설정하면 페이지가 해당 브랜치에 대해 설정된다.


참고

'Web > React' 카테고리의 다른 글

리액트 프로젝트 사이트맵 생성  (0) 2021.04.02
리액트 IE11 크로스 브라우징 설정  (0) 2021.04.02
Web/Django 2021. 8. 11. 23:14

Django 채팅 앱 만들기


장고 기본 페이지 생성

개발 환경

  • 윈도우10, Anaconda3 가상환경에서 수행했다.

    $ conda --version
    conda 4.10.1
    $ conda create -n chat python=3.8
    $ conda activate chat
    $ python --version
    Python 3.8.11
  • 기본적으로 필요한 라이브러리들을 설치한다.

    $ pip install Django
    $ python -m django --version
    3.2.6
    $ pip install channels
    $ python -c 'import channels; print(channels.__version__)'
    3.0.4

프로젝트 생성

  • 작업할 chat 폴더로 들어가서 프로젝트를 생성한다.

    $ django-admin startproject mysite .
    • .은 현재 폴더를 프로젝트의 루트 디렉토리로 구성하라는 의미이다.

      • 생략하면 프로젝트명으로 하위 폴더가 만들어진다.
  • chat 앱을 생성한다.

    $ python manage.py startapp chat
  • mysite/settings.pyINSTALLED_APPS에 방금 생성한 앱을 추가한다.

    mysite/settings.py

    INSTALLED_APPS = [
      'chat',
      'django.contrib.admin',
      'django.contrib.auth',
      'django.contrib.contenttypes',
      'django.contrib.sessions',
      'django.contrib.messages',
      'django.contrib.staticfiles',
    ]
  • 제공되는 기본 html 파일을 chat/templates/chat/index.html 에 입력한다.

    chat/templates/chat/index.html

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8"/>
        <title>Chat Rooms</title>
    </head>
    <body>
        What chat room would you like to enter?<br>
        <input id="room-name-input" type="text" size="100"><br>
        <input id="room-name-submit" type="button" value="Enter">
    
        <script>
            document.querySelector('#room-name-input').focus();
            document.querySelector('#room-name-input').onkeyup = function(e) {
                if (e.keyCode === 13) {  // enter, return
                    document.querySelector('#room-name-submit').click();
                }
            };
    
            document.querySelector('#room-name-submit').onclick = function(e) {
                var roomName = document.querySelector('#room-name-input').value;
                window.location.pathname = '/chat/' + roomName + '/';
            };
        </script>
    </body>
    </html>
  • chat/views.py 에서 index.html을 렌더링하는 함수를 생성하고 URLconf를 설정한다.

    chat/views.py

    from django.shortcuts import render
    
    def index(request):
        return render(request, 'chat/index.html')

    chat/urls.py

    from django.urls import path
    
    from . import views
    
    urlpatterns = [
        path('', views.index, name='index'),
    ]

    mysite/urls.py

    from django.contrib import admin
    from django.urls import path, include
    
    urlpatterns = [
        path('chat/', include('chat.urls')),
        path('admin/', admin.site.urls),
    ]
  • 서버를 돌리면 마이그레이션 하라고 뜨는데, 일단 정상적으로 작동하는지 확인한다.

    $ python3 manage.py runserver
    Django version 3.2.6, using settings 'mysite.settings'
    Starting development server at http://127.0.0.1:8000/
    Quit the server with CTRL-BREAK.

Channels 라이브러리 적용

  • Channels의 라우팅은 장고의 URLconf와 비슷하다.

    • HTTP 요청이 들어오면 장고는 root URLconf에서 view 함수를 찾아서 요청을 처리한다.

    • 비슷하게, Channels는 웹 소켓 연결을 받아서 root routing configuration에서 consumer를 찾는다.

      • consumer의 다양한 함수를 통해 연결에서 오는 이벤트들을 처리한다.
  • mysite/asgi.py를 다음과 같이 수정한다.

    mysite/asgi.py

    import os
    
    from channels.routing import ProtocolTypeRouter
    from django.core.asgi import get_asgi_application
    
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
    
    application = ProtocolTypeRouter({
        "http": get_asgi_application(),
        # Just HTTP for now. (We can add other protocols later.)
    })
  • Channels 라이브러리를 mysite/settings.pyINSTALLED_APPS 에 추가하고, 루트 라우팅 설정도 해준다.

    mysite/settings.py

    INSTALLED_APPS = [
        'channels',
        'chat',
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
    ]
    # Channels
    ASGI_APPLICATION = 'mysite.asgi.application'
    • 맨 위에 추가하지 않으면 충돌이 발생한다고 한다.
  • 서버를 실행하면 아까와 다른 문구가 뜨는 것을 볼 수 있다.

    python manage.py runserver
    Django version 3.2.6, using settings 'mysite.settings'
    Starting ASGI/Channels version 3.0.4 development server at http://127.0.0.1:8000/
    Quit the server with CTRL-BREAK.
  • http://127.0.0.1:8000/chat/ 사이트는 여전히 정상적으로 작동한다.


서버 실행

  • 위에서 생성한 http://127.0.0.1:8000/chat/ 페이지에서 방 제목을 입력하면 해당 방으로 들어가서 채팅하는 앱을 만드는 것이 목표이다.

  • 방에 해당하는 템플릿과 뷰를 작성하고 URLconf를 설정한다.

    chat/templates/chat/room.html

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8"/>
        <title>Chat Room</title>
    </head>
    <body>
        <textarea id="chat-log" cols="100" rows="20"></textarea><br>
        <input id="chat-message-input" type="text" size="100"><br>
        <input id="chat-message-submit" type="button" value="Send">
        {{ room_name|json_script:"room-name" }}
        <script>
            const roomName = JSON.parse(document.getElementById('room-name').textContent);
    
            const chatSocket = new WebSocket(
                'ws://'
                + window.location.host
                + '/ws/chat/'
                + roomName
                + '/'
            );
    
            chatSocket.onmessage = function(e) {
                const data = JSON.parse(e.data);
                document.querySelector('#chat-log').value += (data.message + '\n');
            };
    
            chatSocket.onclose = function(e) {
                console.error('Chat socket closed unexpectedly');
            };
    
            document.querySelector('#chat-message-input').focus();
            document.querySelector('#chat-message-input').onkeyup = function(e) {
                if (e.keyCode === 13) {  // enter, return
                    document.querySelector('#chat-message-submit').click();
                }
            };
    
            document.querySelector('#chat-message-submit').onclick = function(e) {
                const messageInputDom = document.querySelector('#chat-message-input');
                const message = messageInputDom.value;
                chatSocket.send(JSON.stringify({
                    'message': message
                }));
                messageInputDom.value = '';
            };
        </script>
    </body>
    </html>

    chat/views.py

    from django.shortcuts import render
    
    def index(request):
        return render(request, 'chat/index.html', {})
    
    def room(request, room_name):
        return render(request, 'chat/room.html', {
            'room_name': room_name
        })

    chat/urls.py

    from django.urls import path
    from . import views
    
    urlpatterns = [
        path('', views.index, name='index'),
        path('<str:room_name>/', views.room, name='room'),
    ]
  • 이대로 서버를 실행해서 http://127.0.0.1:8000/chat/ 에 접속하고 방 제목을 입력해보면 해당 페이지로 이동되는 것을 볼 수 있다.

    • 하지만 이동하는 순간 서버에는 다음과 같은 오류가 발생한다.

      ValueError: No application configured for scope type 'websocket'  
    • 클라이언트에서도 콘솔 창을 열어보면 다음과 같은 오류가 발생한 것을 볼 수 있다.

      WebSocket connection to 'ws://127.0.0.1:8000/ws/chat/asd/' failed: 
      Chat socket closed unexpectedly
    • html 파일의 자바스크립트에서 WebSocket을 연결하는 것에 실패했기 때문이다.

  • 해당 URL에 대한 웹 소켓을 받아주는 consumer가 필요하다.

    chat/consumers.py

    import json
    from channels.generic.websocket import WebsocketConsumer
    
    class ChatConsumer(WebsocketConsumer):
        def connect(self):
            self.accept()
    
        def disconnect(self, close_code):
            pass
    
        def receive(self, text_data):
            text_data_json = json.loads(text_data)
            message = text_data_json['message']
    
            self.send(text_data=json.dumps({
                'message': message
            }))
    • receive()

      • 위의 chat/templates/chat/room.html 파일에서 document.querySelector('#chat-message-submit').onclick 이벤트를 설정했었다.

        • 이 함수에서 보낸 json 데이터를 다시 클라이언트에게 되돌려주는 역할을 한다.
      • 클라이언트가 메시지를 다시 받으면 chat/templates/chat/room.html 파일의 chatSocket.onmessage 이벤트에서 로그를 추가하는 방식이다.

    • 이 컨슈머는 동기적(synchronous)이다.

      • 비동기적(asynchronous)으로 구현하는게 훨씬 성능이 좋지만, 장고 모델 접근에서 문제가 발생할 수 있으니 Consumer 문서를 잘 참고하라고 써있다.
  • 이 consumer를 이용하기 위해서 routing configuration을 설정해야 한다.

    chat/routing.py

    from django.urls import re_path
    from . import consumers
    
    websocket_urlpatterns = [
        re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
    ]
    • 각각의 연결에 consumer의 인스턴스를 생성하기 위해 as_asgi() 클래스메서드를 사용했다.

      • 장고 뷰 인스턴스의 per-request와 같은 역할이라는데 이건 또 공부해봐야겠다.
    • re_path() 함수를 사용한 것은, 만약 내부 라우터가 미들웨어에 감싸져 있을 경우 path() 함수는 제대로 동작하지 않을 수 있기 때문이다.

  • 위에서 작성한 설정을 root routing configuration에서 지정한다.

    mysite/asgi.py

    import os
    
    from channels.auth import AuthMiddlewareStack
    from channels.routing import ProtocolTypeRouter, URLRouter
    from django.core.asgi import get_asgi_application
    import chat.routing
    
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
    
    application = ProtocolTypeRouter({
      "http": get_asgi_application(),
      "websocket": AuthMiddlewareStack(
            URLRouter(
                chat.routing.websocket_urlpatterns
            )
        ),
    })
    • 서버로 연결이 생성되면 먼저 ProtocolTypeRouter가 연결의 종류를 탐색한다.

    • ws:// 혹은 wss:// 형식의 웹 소켓 연결이면, 해당 연결은 AuthMiddlewareStack으로 메시지를 group의 모든 channel에게 보낸다.

  • chat_message()

    • channel layer의 group으로부터 전달된다.

    • AuthMiddlewareStack은 현재 인증된 유저를 참조하여 연결의 scope를 채운다.

      • 장고의 AuthenticationMiddleware이 현재 인증된 유저를 참조하여 view 함수의 request 객체를 채우는 것과 비슷하다고 한다.
    • 그 후 연결은 URLRouter로 전달된다.

  • 여기까지 하고 migration 후 서버를 돌렸더니 오류가 발생했다.

    $ python manage.py migrate
    $ python manage.py runserver
    django.core.exceptions.ImproperlyConfigured: Cannot import ASGI_APPLICATION module 'mysite.asgi'
  • 이제 http://127.0.0.1:8000/chat/a 등의 주소로 이동해서 메시지를 입력해보면 해당 메시지가 성공적으로 되돌아오는 것(echo)을 볼 수 있다.

    • 같은 주소로 창을 두 개 열어서 확인해보면 다른 쪽에는 메시지가 전송되지 않는 것을 볼 수 있다.

    • 이를 해결하려면 ChatConsumer의 여러 인스턴스가 서로 통신하도록 해야 한다.

    • Channels는 컨슈머끼리 이런 통신을 할 수 있도록 하는 channel layer를 제공한다.

channel layer 활성화

  • channel layer는 다음과 같은 추상화를 제공한다.

    • channel

      • 메시지가 전달될 수 있는 우체통이다.

      • 각 channel은 이름을 가지며, 다른 channel에게 메시지를 전송할 수 있다.

    • group

      • 연관된 channel들의 그룹이다.

      • group은 이름을 가지며, channel의 이름을 통해 추가하고 삭제할 수 있다.

      • 그룹의 모든 channel에게 메시지를 전송할 수 있다.

      • 특정 그룹에 있는 channel들을 나열할 수는 없다.

  • 모든 consumer 인스턴스는 자동으로 유일한 channel name을 생성하기 때문에, 서로 channel layer를 통해 통신할 수 있다.

  • 지금 진행 중인 앱에서는 같은 방에 있는 ChatConsumer의 여러 인스턴스들끼리 통신하는게 목표이다.

    • 이를 위해 각 ChatConsumer 인스턴스는 room name을 이름으로 하는 group에 channel을 추가한다.

    • 이러면 같은 방에 있는 channel들은 같은 group에 속하므로 서로 통신할 수 있게 된다.

  • 도커의 redis 컨테이너를 backing store로 사용하는 channel layer를 생성할 것이다.

    $ docker run -p 6379:6379 -d redis:5
    • redis컨테이너는 6379 포트를 사용한다.

    • redis 5.0 버전을 사용하는 이유는 아마...

      https://redis.io/download#other-versions

      Redis 5.0 is the first version of Redis to introduce the new stream data type with consumer groups, ...
  • Channels와 redis를 연결하기 위해 라이브러리를 설치한다.

    $ pip install channels_redis
  • channel layer를 사용하기 위해 mysite/settings.pyASGI_APPLICATION 밑에 다음과 같이 백엔드와 호스트에 대한 설정을 추가한다.

    # Channels
    ASGI_APPLICATION = 'mysite.asgi.application'
    CHANNEL_LAYERS = {
        'default': {
            'BACKEND': 'channels_redis.core.RedisChannelLayer',
            'CONFIG': {
                "hosts": [('127.0.0.1', 6379)],
            },
        },
    }
  • channel layer와 redis의 연결을 확인하기 위해 다음과 같이 입력해본다.

    $ python manage.py shell
    >>> import channels.layers
    >>> channel_layer = channels.layers.get_channel_layer()
    >>> from asgiref.sync import async_to_sync
    >>> async_to_sync(channel_layer.send)('test_channel', {'type': 'hello'})
    >>> async_to_sync(channel_layer.receive)('test_channel')
    {'type': 'hello'}
    >>> exit()
  • 이제 channel layer가 있으므로, ChatConsumer를 수정하여 group 내의 channel에게 보내도록 설정한다.

    chat/consumers.py

    import json
    from asgiref.sync import async_to_sync
    from channels.generic.websocket import WebsocketConsumer
    
    class ChatConsumer(WebsocketConsumer):
        def connect(self):
            self.room_name = self.scope['url_route']['kwargs']['room_name']
            self.room_group_name = 'chat_%s' % self.room_name
    
            # Join room group
            async_to_sync(self.channel_layer.group_add)(
                self.room_group_name,
                self.channel_name
            )
    
            self.accept()
    
        def disconnect(self, close_code):
            # Leave room group
            async_to_sync(self.channel_layer.group_discard)(
                self.room_group_name,
                self.channel_name
            )
    
        # Receive message from WebSocket
        def receive(self, text_data):
            text_data_json = json.loads(text_data)
            message = text_data_json['message']
    
            # Send message to room group
            async_to_sync(self.channel_layer.group_send)(
                self.room_group_name,
                {
                    'type': 'chat_message',
                    'message': message
                }
            )
    
        # Receive message from room group
        def chat_message(self, event):
            message = event['message']
    
            # Send message to WebSocket
            self.send(text_data=json.dumps({
                'message': message
            }))
    • connect()

      • 연결 시 channel layer의 group에 channel을 추가한다.

      • self.room_namechat/urls.py에서 지정한 변수명을 AuthMiddlewareStack가 scope에 kwargs 형태로 채워준 것을 꺼내서 사용한다.

      • 실제로 channel layer에서 사용하는건 self.room_group_name이다.

        • 굳이 이렇게 변환하는 이유는..?
    • disconnect()

      • 연결 해제 시 channel layer의 group에서 channel을 삭제한다.
    • receive()

      • 웹 소켓으로부터 메시지를 받을 시 메시지를 channel layer의 group 내 모든 channel에게 보낸다.
    • chat_message()

      • channel layer의 group으로부터 메시지를 받을 시 메시지를 클라이언트로 전달한다.

      • 전과 마찬가지로 chatSocket.onmessage 이벤트에 의해 처리된다.

  • 서버를 실행하고 클라이언트 두 개로 통신해보면 정상적으로 작동하는 것을 볼 수 있다.

Web/React 2021. 4. 2. 22:44

리액트 프로젝트 사이트맵 생성

  • 구글 검색을 최적화하려면 사이트맵을 제출해야 한다.

  • yarnnpm을 혼용했는데 웬만하면 하나로 통일해서 사용하는게 좋은 듯하다.

  • react-router-sitemap 패키지를 사용한다.

      npm install react-router-sitemap
  • 사이트에서 Route를 사용한 파일을 복사하여 이름을 변경한다.

  • 적절한 경로에 사이트맵 제네레이터 파일을 생성한다.

    sitemapGenerator.js

      require("babel-register")({
          presets: ["es2015", "react"]
      });
      require.extensions['.css'] = () => {};
    
      const router = require("./sitemapRoutes").default;
      const Sitemap = require("react-router-sitemap").default;
    
      function generateSitemap() {
              return (
                  new Sitemap(router)
                          .build("도메인 이름")
                          .save("./public/sitemap.xml")
              );
      }
    
      generateSitemap();
    • 따로 설치한 패키지의 css 파일에서 SyntaxError: Invalid left-hand side in assignment 에러가 발생해서, require.extensions['.css'] = () => {}; 항목을 넣어주었다.
  • 위의 바벨을 적용하기 위해 다음 패키지를 설치한다.

      npm install --save-dev babel-cli 
      npm install --save-dev babel-preset-es2015 
      npm install --save-dev babel-preset-react 
      npm install --save-dev babel-register
  • package.json 파일의 scripts 부분에 다음을 추가하여 사용한다.

      ... 
          "scripts": {     
              ...
              "sitemap": "babel-node 경로/sitemaGenerator.js"   
          } 
      ...
  • yarn sitemap을 실행하면 sitemap.xml 파일이 생긴다.

  • 배포할 때 자동으로 생성되도록 하려면 package.json 파일의 scripts 부분에 다음을 추가한다.

      ... 
          "scripts": {     
              ...    
              "predeploy": "yarn sitemap"   
          } 
      ...

'Web > React' 카테고리의 다른 글

리액트 gh-pages 적용  (0) 2021.08.18
리액트 IE11 크로스 브라우징 설정  (0) 2021.04.02
Web/React 2021. 4. 2. 22:43

리액트 IE11 크로스 브라우징 설정

  • 리액트로 만든 사이트가 IE11에서 작동하지 않는 현상이 있었다.

  • 나는 create-react-app을 사용했다.

  • 루트 디렉토리에서 패키지 매니저로 react-app-polyfill을 설치한다.

      yarn add react-app-polyfill
  • index.js에서 라이브러리 불러오기

      import 'react-app-polyfill/ie11';
      import 'react-app-polyfill/stable';

참고

'Web > React' 카테고리의 다른 글

리액트 gh-pages 적용  (0) 2021.08.18
리액트 프로젝트 사이트맵 생성  (0) 2021.04.02
Web/Nginx 2021. 3. 9. 02:57

Nginx SPA에서 새로고침 시 404 Not Found 에러 발생

  • SPA(Single Page Application)

    • 새로운 페이지를 불러오지 않고, 현재 페이지를 동적으로 다시 작성하는 어플리케이션이나 웹 사이트

    • 내 경우엔 리액트로 빌드한 웹사이트였다. 원인이 리액트 소스코드에 있는 줄 알았는데 Nginx 문제였다.


원인

  • 하위주소에 대한 html 파일을 요청하는데, SPA의 경우 index.html 내부에서 처리하므로 이외의 파일을 찾을 수 없다.

해결

  • 파일을 찾아도 없으면 index.html 파일로 연결하도록 설정해야 한다.

  • nginx 설정 파일(/etc/nginx/conf.d/default.conf 혹은 /etc/nginx/sites-available/default)에서 try_files를 다음과 같이 설정한다.

    location / {
      try_files $uri $uri/ /index.html;
    }

참고

Web 2021. 2. 21. 22:59

티스토리 마크다운 CSS 적용

  • 마크다운으로 글을 썼더니 typora와 다르게 표시되고 테이블이 깨진다.

  • github-markdown.css를 적용하기로 결정했다.


적용 과정

  • 공식 깃허브 링크

    https://github.com/sindresorhus/github-markdown-css/blob/main/github-markdown.css

  • 위 링크의 코드를 복사하여 VSCode 등의 에디터에 붙여넣기한다.

    • 같은 단어를 한번에 변경할 수 있어야 편하다.
  • .markdown-body로 작성된 부분을 드래그로 긁어서 우클릭한다.

    • Change All Occurences(Ctrl + F2)로 같은 부분을 한번에 편집할 수 있다.

    • 적용하고 싶은 클래스명을 입력

      • 크롬 등의 브라우저에서 개발자 모드로, 작성한 글이 전부 포함된 DOM의 클래스명 확인

      • 내가 적용한 블로그 스킨같은 경우 글을 쓰면 tt_article_useless_p_margin 클래스의 하위에 작성됨

  • 전부 복사해서 블로그 스킨 편집의 html 편집에서 CSS 항목 맨 밑에 붙여넣기

    • 혹은 파일로 저장하여 업로드 후 html<head> 부분에서 <link>로 적용해도 될 것 같다.
  • 카테고리의 다른 글 처리

    • 이것도 테이블 형식이라 마크다운의 테이블 형식으로 변환되어서 깨진다.

    • CSS 탭 맨 아래에 다음 내용을 추가했다.

      .tt_article_useless_p_margin .another_category h4 {
        text-align: center;
      }
      .tt_article_useless_p_margin .another_category tbody {
        display: block;
      }
      .tt_article_useless_p_margin .another_category table,
      .tt_article_useless_p_margin .another_category tbody,
      .tt_article_useless_p_margin .another_category tr,
      .tt_article_useless_p_margin .another_category th,
      .tt_article_useless_p_margin .another_category td {
        border: none;
        background-color: white;
      }
      .tt_article_useless_p_margin .another_category tr {
        display: flex;
        justify-content: space-between;
      }
      .tt_article_useless_p_margin table tr:nth-child(2n) {
        background-color: white;
      }

참고