일반 사용자 계정에서는 GRANT OPTION(또는 WITH GRANT OPTION)을 없애고,
필요한 “권한 위임 기능”은 ROLE을 통해서만 하도록 구조를 바꾸면 됩니다.

 

1. GRANT OPTION 가진 일반 사용자 계정 찾기

DBA / 시스템 계정이 아닌데 Grant_priv='Y'인 계정이 바로 조치 대상

 

사용자별 부여된 Role 확인 방법

SHOW GRANTS FOR '<User>'@'<Host>';

 

 

2. 일반 사용자에게서 GRANT OPTION 제거

조치 대상 계정에 대해 직접 가진 GRANT OPTION을 제거

 

 

3. ROLE 기반 구조로 재설계 (WITH GRANT OPTION은 ROLE에만)

이제 권한 자체는 ROLE에 부여하고,
일반 사용자는 ROLE만 부여받아서 쓰도록 만드는 단계

 

4. 일반 사용자에게는 ROLE만 부여 (직접 GRANT OPTION 없음)

 

 

5. 검증 단계 (정책 준수 여부 확인)

 

 

 

 

 

 

 

 

 

 

 

'#개발 > 데이터베이스' 카테고리의 다른 글

INNER JOIN과 OUTER JOIN의 차이  (0) 2021.08.23
관계대수와 관계해석  (0) 2021.07.15
데이터 무결성  (0) 2021.07.14
데이터베이스의 설계 단계  (0) 2021.07.14
스키마 정의 및 종류  (0) 2021.07.14

1. 구독

 

구독 등록 명령어

subscription-manager register --username <username> --password <password> --auto-attach --force

 

 

2. yum 캐시 관리

  | yum 캐시란

  | yum 캐시 삭제

    || yum 캐시 삭제 명령어 yum clean all

    || 명령어 의미 : yum의 모든 캐시 파일을 완전히 삭제

    || 삭제 대상

캐시 종류 설명
metadata repository 메타데이터
packages 다운로드된 rpm 파일
dbcache 캐시된 sqlite DB
headers (구버전) 패키지 헤더
expired files 만료된 repodata

    || 실행 시점

 

  • repo URL이 변경됨
  • 메타데이터 손상됨
  • “cannot find valid baseurl” 오류 발생
  • 404, 403 등 mirror 문제 발생
  • 패키지를 최신 상태로 다시 받고 싶을 때

 

  | yum 캐시 재생성

    || yum 캐시 재생성 명령어 yum makecache

 

 

 

 

 

'#시스템관리 > 리눅스' 카테고리의 다른 글

Ubuntu에 자바 설치  (0) 2020.11.26
Ubuntu 버전 확인 방법, 비트 확인 방법  (0) 2020.11.26

 JPA에서 복합키를 사용하려면 별도의 ID 클래스가 필요

package com.poc_lotte_chatbot.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@IdClass(ResocTopicRelId.class)
@Entity
@Table(name = "resoc_topic_rel", schema = "chatbi")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ResocTopicRel {

    @Column(name = "org_party_id", nullable = false, length = 10)
    private String orgPartyId;

    @Id
    @Column(name = "resoc_id", nullable = false, precision = 10)
    private BigDecimal resocId;

    @Column(name = "resoc_gubun_code", nullable = false, length = 5)
    private String resocGubunCode;

    @Column(name = "resoc_name", nullable = false, length = 100)
    private String resocName;

    @Column(name = "prompt_topic_type", length = 100)
    private String promptTopicType;

    @Id
    @Column(name = "hist_str_date", nullable = false)
    private LocalDateTime histStrDate;

    @Id
    @Column(name = "hist_end_date", nullable = false)
    private LocalDateTime histEndDate;
}

역할:
  - 실제 데이터베이스 테이블을 매핑하는 엔티티 클래스
  - 테이블의 모든 컬럼(PK + 일반 컬럼)을 포함

  특징:
  - @IdClass(ResocTopicRelId.class): 복합키 클래스를 지정
  - @Id 어노테이션이 3개: 복합키를 구성하는 각 필드에 표시
  - 일반 컬럼들도 포함: orgPartyId, resocGubunCode, resocName, promptTopicType

  쓰임:
  - CRUD 작업의 대상
  - 비즈니스 로직에서 데이터를 담는 객체
  - Repository를 통한 데이터 조회/저장

 

package com.poc_lotte_chatbot.entity;

import jakarta.persistence.Embeddable;
import lombok.*;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Embeddable
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class ResocTopicRelId implements Serializable {
    private BigDecimal resocId;
    private LocalDateTime histStrDate;
    private LocalDateTime histEndDate;
}

  역할:
  - 테이블의 복합 기본키(Composite Primary Key)를 표현하는 클래스
  - resoc_topic_rel 테이블의 PK는 3개 컬럼으로 구성: (resoc_id, hist_str_date, hist_end_date)
  - JPA에서 복합키를 사용하려면 별도의 ID 클래스가 필요

  특징:
  - @Embeddable: 다른 엔티티에 포함될 수 있는 클래스임을 표시
  - Serializable 구현: JPA 복합키 요구사항
  - equals()와 hashCode() 구현 필요: @EqualsAndHashCode로 자동 생성

  쓰임:
  - 엔티티의 고유 식별자 역할
  - 엔티티 조회, 비교, 해싱 작업에 사용
  - Repository에서 findById(ResocTopicRelId id) 형태로 사용

'#개발 > JPA' 카테고리의 다른 글

JPA 메서드 네이밍 규칙  (0) 2025.10.31
Spring Data JPA Query Method  (0) 2025.10.23

원인
npm install 실행 시, git:// 프로토콜을 이용하여 Github 저장소에 접근하지 못할 때 발생하는 오류

 

조치 방법

git config --global url."https://".insteadOf git://

 

 

'#개발 > node.js' 카테고리의 다른 글

node 애플리케이션  (0) 2025.10.19

npm config get registry

: 현재 npm이 패키지를 다운로드할 패키지 저장소의 URL을 확인하는 명령어

ex. https://registry.npmjs.org/

 

npm 레지스트리란

모든 npm 패키지를 저장하는 중앙 저장소

기번 저장소는 https://registry.npmjs.org/ 이며, 전세계 많은 개발자들이 패키지를 공유하고 재사용하는 곳

`npm install <패키지명>` 명령을 실행하면, npm은 패키지를 이 레지스트리에서 해당 패키지를 다운로드 받

`npm install` 이 내부적으로 하는 일 

npm install 은 크게 두 종류의 의존성을 처리합니다:

구분예시설치 방식OS 영향 여부
순수 JS 패키지 lodash, axios, moment 등 .js 파일만 다운로드 ❌ 영향 없음
네이티브 애드온 패키지 node-sass, bcrypt, sqlite3, canvas, sharp 등 C/C++ 소스를 node-gyp 로 빌드 → .node 바이너리 생성 ✅ OS·Node·컴파일러·라이브러리 모두 영향받음

즉, 순수 JS 모듈은 어디서 설치해도 동일하지만,
네이티브 모듈은 빌드된 결과(.node)가 OS·CPU·Node 버전에 따라 다릅니다.

 

 

 

js 파일을 실행시키는 것은 문제가 안되지만, C/C++로 작성된 확장 모듈 (ex. glibc, OpenSSL, GCC, python) 은 OS와 런타임의 ABI에 직접 의존하기 때문에 로드하는 Node 프로세스는 순간 오류가 나거나 죽을 수 있다.

npm install 시 이 모듈들은 c++로 빌드가 된다.

즉, npm install을 어느 OS에서 진행하냐에 따라 애드온의 결과가 달라진다.

다만 모든 패키지가 그런 것은 아니고 'C/C++로 빌드가 필요한 모듈'만 OS에 영향을 받는다.

 

대표적인 네이티브 애드온

Node 프로젝트에는 **C/C++로 작성된 확장 모듈(네이티브 애드온)**이 종종 들어있습니다.
대표적인 예가 아래입니다.

모듈명 설명 특징
node-sass SCSS를 C++로 컴파일 libsass.so 등 네이티브 코드
bcrypt 암호화 알고리즘 구현 OpenSSL, glibc 의존
canvas, sqlite3, fsevents 등 시스템 라이브러리 직접 호출 OS/컴파일러와 강하게 묶임

 

빌드가 OS별로 달라지는 이유

(1) node-gyp 빌드는 OS의 “컴파일러 + 시스템 라이브러리”를 사용

  • Linux → gcc, glibc, libstdc++, OpenSSL
  • macOS → clang, libc++, CommonCrypto
  • Windows → MSVC, msvcrt.dll

→ 각 플랫폼마다 ABI(Application Binary Interface)심볼 이름이 다름.
따라서 동일한 C++ 코드라도 다른 바이너리 형태로 컴파일됩니다.

예:

 
build/Release/bcrypt_lib.node # Linux build/Release/bcrypt_lib.node # macOS build/Release/bcrypt_lib.node # Windows

파일 이름은 같지만 내부 구조가 완전히 다름.

(2) Node 버전에 따라 ABI 버전(NODE_MODULE_VERSION)이 다름

Node 10 = ABI 64, Node 12 = ABI 72, Node 16 = ABI 93 …
→ Node10에서 빌드한 모듈은 Node16에서 로드 불가.

에러 예시:

 
Error: The module '/.../binding.node' was compiled against a different Node.js version using NODE_MODULE_VERSION 64.

 

(3) OS 버전(glibc 차이)에 따라 심볼 링크 달라짐

  • RHEL7 glibc 2.17
  • RHEL9 glibc 2.34
    → RHEL9에서 빌드하면 GLIBC_2.28 이상 심볼 사용 → RHEL7에서는 “not found” 오류 발생
 
/lib64/libc.so.6: version `GLIBC_2.28' not found

 

 

예시: bcrypt 설치 결과 비교

환경결과 바이너리glibc/ABI 의존성
RHEL7 + Node10 bcrypt_lib.node (ELF, glibc 2.17) 낮은 glibc 심볼
RHEL9 + Node10 bcrypt_lib.node (ELF, glibc 2.34) 높은 glibc 심볼
Windows + Node10 bcrypt_lib.node (DLL, PE/COFF) msvcrt 의존

모양은 같지만 내부 코드 완전히 다름.
따라서 한 OS에서 만든 node_modules를 다른 OS에 옮기면 작동이 안 될 수 있음.

 

 

확인 방법

(1) 네이티브 모듈 찾기

 
find node_modules -name '*.node' -print

(2) 빌드 환경 확인

 
file node_modules/**/binding.node ldd node_modules/**/binding.node | egrep 'libc|stdc++' node -p "process.versions.modules"

 

 

OS별 빌드 차이를 피하는 방법

 

전략 설명 비고
① 사전 빌드(prebuild) 패키지 제작자가 OS별로 미리 빌드해 배포 (prebuild-install 사용) 가장 안전. 단, 폐쇄망에서는 다운로드 불가 가능
② 동일 OS 환경에서 빌드 운영환경(RHEL9 등)과 동일한 OS 버전에서 npm install 실행 내부망 배포 시 재현성↑
③ Docker / CI 빌드 이미지 고정 node:10-buster 등 고정 이미지로 항상 동일 glibc에서 빌드 RHEL9 서버에서도 호환
④ JS-only 대체 사용 bcrypt → bcryptjs, node-sass → sass 빌드 필요 없음

 

'#시스템관리 > 마이그레이션' 카테고리의 다른 글

레지스트리란  (0) 2025.11.09

  복합키(@EmbeddedId)의 내부 필드에 접근할 때는 **언더스코어(_)**를 사용합니다:

  - Id → 복합키 필드명 (PrdtMstrId)
  - Id_PrdtId → 복합키 내부의 prdtId 필드
  - Id_HistStrDate → 복합키 내부의 histStrDate 필드

  따라서:
  - findFirstById_PrdtId → id.prdtId로 조회
  - OrderById_HistStrDateDesc → id.histStrDate로 내림차순 정렬
  - findFirst → 첫 번째 결과만 반환

  이제 @Query 없이도 JPA가 자동으로 쿼리를 생성합니다!

1. Spring Data JPA Query Method란 ?

메서드 이름만으로 자동으로 쿼리를 생성해주는 기능

  // Repository 인터페이스에 메서드 선언만 하면
  boolean existsByCrtPartyIdAndOriginalResocIdAndIsSharedAndHistEndDate(
      BigDecimal crtPartyId,
      BigDecimal originalResocId,
      String isShared,
      LocalDateTime histEndDate
  );

  // Spring Data JPA가 자동으로 SQL을 생성하여 실행

2. 메서드 이름 분석

메서드 이름은 규칙에 따라 구성됩니다:

existsBy + CrtPartyId + And + OriginalResocId + And + IsShared + And + HistEndDate
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
동작 + 필드명 + 조건 + 필드명 + 조건 + 필드명 + 조건 + 필드명

각 부분 설명:

부분 의미 설명
exists 존재 여부 확인 해당 조건의 레코드가 있으면 true, 없으면 false
By 조건 시작 WHERE 절 시작
CrtPartyId 필드명 ResocMstr.crtPartyId
And 논리 연산자 SQL의 AND
OriginalResocId 필드명 ResocMstr.originalResocId
And 논리 연산자 SQL의 AND
IsShared 필드명 ResocMstr.isShared
And 논리 연산자 SQL의 AND
HistEndDate 필드명 ResocMstr.histEndDate

3. 자동 생성되는 SQL

  SELECT
      CASE WHEN COUNT(*) > 0 THEN TRUE ELSE FALSE END
  FROM chatbi.resoc_mstr
  WHERE crt_party_id = ?          -- 첫 번째 파라미터
    AND original_resoc_id = ?     -- 두 번째 파라미터
    AND is_shared = ?             -- 세 번째 파라미터
    AND hist_end_date = ?         -- 네 번째 파라미터

4. 실제 동작 예시

  // Service에서 호출
  private boolean isDuplicateShare(BigDecimal originalResocId, BigDecimal targetOrgId) {
      return resocMstrRepository.existsByCrtPartyIdAndOriginalResocIdAndIsSharedAndHistEndDate(
          targetOrgId,              // crtPartyId = 10
          originalResocId,          // originalResocId = 100
          "Y",                      // isShared = 'Y'
          MAX_HIST_END_DATE         // histEndDate = '9999-12-31'
      );
  }

  // 실행되는 SQL
  // SELECT CASE WHEN COUNT(*) > 0 THEN TRUE ELSE FALSE END
  // FROM resoc_mstr
  // WHERE crt_party_id = 10
  //   AND original_resoc_id = 100
  //   AND is_shared = 'Y'
  //   AND hist_end_date = '9999-12-31 00:00:00'

  // 결과: 레코드가 존재하면 true, 없으면 false

existsByCrtPartyIdAndOriginalResocIdAndIsSharedAndHistEndDate 사용 목적

중복 공유 방지를 위해 사용합니다:

  // 상황: 조직 10에게 메뉴 100을 공유하려고 함

  // 체크 로직
  boolean isDuplicate = isDuplicateShare(100, 10);

  if (isDuplicate) {
      // 이미 조직 10에게 메뉴 100(또는 그 복사본)이 공유되어 있음
      // → 재공유 방지
      throw new DuplicateShareException("이미 공유된 자원입니다.");
  }
  // 데이터 예시
  // resoc_mstr 테이블:
  // resoc_id | crt_party_id | original_resoc_id | is_shared | hist_end_date
  // ---------|--------------|-------------------|-----------|---------------
  // 500      | 10           | 100               | Y         | 9999-12-31    ← 이미 존재!
  //
  // → isDuplicate = true

6. Query Method의 다른 예시

// 1. 단순 조회
List findByCrtPartyId(BigDecimal crtPartyId);
// → WHERE crt_party_id = ?

// 2. 정렬
List findByCrtPartyIdOrderBySortSeq(BigDecimal crtPartyId);
// → WHERE crt_party_id = ? ORDER BY sort_seq

// 3. 범위 조회
List findByResocIdBetween(BigDecimal start, BigDecimal end);
// → WHERE resoc_id BETWEEN ? AND ?

// 4. Like 검색
List findByResocNameContaining(String keyword);
// → WHERE resoc_name LIKE %?%

// 5. Count
long countByCrtPartyIdAndIsShared(BigDecimal crtPartyId, String isShared);
// → SELECT COUNT(*) WHERE crt_party_id = ? AND is_shared = ?

7. Query Method vs @Query

Query Method (자동 생성)

boolean existsByCrtPartyIdAndOriginalResocId(
BigDecimal crtPartyId,
BigDecimal originalResocId
);

장점:

  • 구현 불필요, 선언만 하면 됨
  • 타입 안정성 보장
  • 메서드 이름으로 의도 명확
  • 단점:
  • 복잡한 쿼리는 메서드 이름이 너무 길어짐
  • Join이나 서브쿼리 등 복잡한 로직 구현 어려움@Query("""
    SELECT CASE WHEN COUNT(*) > 0 THEN TRUE ELSE FALSE END
    FROM ResocMstr r
    WHERE r.crtPartyId = :crtPartyId""")
    boolean checkDuplicateShare(
    @Param("crtPartyId") BigDecimal crtPartyId,
    @Param("originalResocId") BigDecimal originalResocId,
    @Param("isShared") String isShared
    );
  • 장점:
  • AND r.originalResocId = :originalResocId AND r.isShared = :isShared
  • @Query (수동 작성)
  • 복잡한 쿼리 작성 가능
  • Join, 서브쿼리, 집계 함수 등 자유롭게 사용
  • 단점:
  • 직접 JPQL/SQL 작성 필요
  • 오타나 문법 오류 가능성

1. 가장 간단한 판단법

① View Page Source (페이지 소스 보기)

  • 브라우저에서 Ctrl+U 또는 우클릭 → “페이지 소스 보기”
  • HTML 내용이 꽉 차 있으면 SSR,
    <div id="app"> 또는 <div id="root">만 있고 내용이 비어 있으면 CSR 입니다.

예시:

  • SSR (서버에서 렌더링된 HTML)
  •  
    <div id="app"> <h1>오늘의 뉴스</h1> <p>실시간 속보...</p> </div>
  • CSR (클라이언트 렌더링)→ 내용이 JS 로드 후에만 보이면 CSR
  •  
    <div id="app"></div> <script src="/assets/app.js"></script>

2. Network 탭에서 확인

  1. F12 → Network 탭 → 새로고침(Ctrl+R)
  2. 첫 번째 요청(document 또는 index.html) 클릭
  3. “Response” 탭에서 HTML을 확인
    • 서버에서 바로 렌더링된 콘텐츠가 있으면 SSR
    • 비어 있고 JS를 불러온 뒤 렌더링되면 CSR

3. Disable JavaScript 후 확인

  1. 크롬 개발자 도구 → Settings(F1) → Debugger → “Disable JavaScript” 체크
  2. 새로고침
    • 화면이 거의 비어 있으면 CSR
    • 콘텐츠가 그대로 보이면 SSR

👉 SSR은 JS 없이도 HTML이 완성되어 있으므로, JS 비활성화 상태에서도 글자가 보입니다.
CSR은 JS 실행이 안 되면 아무 것도 안 보입니다.


4. Lighthouse / Chrome DevTools “Performance” 탭

  • “Main Thread” 타임라인을 보면:
    • HTML 로드 후 JS 실행으로 렌더링 시간 지연 → CSR
    • HTML 로드 시점에 이미 콘텐츠가 표시 → SSR

5. 간단한 JS 콘솔 확인

document.querySelector("#app").innerHTML
  • 비어 있거나 <noscript>만 있으면 CSR
  • HTML 요소가 이미 존재하면 SSR

 

⚡추가 팁

항목 SSR CSR
첫 화면 표시 속도 빠름 (서버 렌더링) 느림 (JS 다운로드 후 렌더링)
SEO (검색엔진 노출) 유리 불리 (JS 실행 필요)
JS 끈 상태에서 내용 보임 내용 없음
배포 방식 Node 서버 필요 정적 파일 CDN 가능

'#개발 > 웹 애플리케이션 아키텍처' 카테고리의 다른 글

SSR, CSR, SPA, MPA  (1) 2025.09.26
OAuth  (0) 2021.09.22
쿠키 & 세션 & JWT  (0) 2021.09.20

pm2 는 javascript 런타임 node.js 프로세스 관리자이다.
사내 서비스 중에 pm2 가 vue 프로젝트를 실행시키고, nginx 가 reverse proxy 역할만 수행하는 서비스가 있다.
node 앱은 백엔드 서비스만 의미하는 줄 알았는데, FE에서도 쓰이는 용어인지 궁금했다.

결론부터 이야기 하면 vue 로 구현된 서비스도 node 앱이 될 수  있다.

즉, node.js 환경에서 실행되는 서비스이면 node 애플리케이션이다.

라이브러리, 프레임워크와 상관이 없는 것이다.

 

node 앱의 의미에 대해서 알아보자.

 

node 앱이란?

node.js 로 실행되는 프로그램

  • node.js: 브라우저 밖에서 javascript를 실행할 수 있게 만든 런타임 환경. 원래 javascript는 브라우저에서만 동작했지만, node.js는 서버나 터미날에서 js를 동작할 수 있게 한다.
  • node app.js 를 하게 되면, app.js 의 javascript 코드가 node.js 위에서 실행된다.

pm2 실행환경으로 본 정체

pm2 자체가 node.js 에서 실행되는 프로그램, 즉 node 앱.
pm2는 여러 node 앱을 관리하는 node 앱
그렇기 때문에 시스템 내에 node.js 가 반드시 설치되어 있어야 한다.

[시스템] → [Node.js 런타임] → [PM2 CLI 스크립트] → [PM2 Daemon] → [Node.js 앱]

pm2 실행 시 내부 동작 구조

  1. pm2 start app.js 명렁어 실행
  2. 운영체제(OS) 는 $PATH 안에 있는 pm2 실행 파일을 찾음
# pm2 실행 파일 내부
#!/usr/bin/env node
require('../lib/ProcessContainerFork.js');

이 파일은 node.js 로 만든 스크립트
#!/usr/bin/env node : node로 실행하라
즉 pm2는 node.js 인터프리터로 실행

  1. node.js 런타임이 pm2 스크립트 해석 -> pm2 내부에서 "app.js를 백그라운드로 실행"하라는 명령을 daemon에 전달
  2. node.js 는 새로운 node.js 프로세스를 fork(자식 프로세스 실행) 하여 아래와 같이 node.js 런타임 실행
node /path/to/app.js

실제 프로세스 구조 확인

pm2 start app.js
ps -ef | grep node
ubuntu   1234  1  0  pm2: God Daemon (/usr/lib/node_modules/pm2/lib/Daemon.js)
ubuntu   1250 1234 0  node /home/user/app.js

PID 1234 : pm2 daemon으로 실행 -> node로 실행 됨
PID 1250 -> pm2 가 fork한 실제 node 앱 -> node로 실행 됨
즉, node.js 가 pm2 자신과 app.js를 모두 구동

요약 구조

┌──────────────────────────────────────────┐
│              Node.js 런타임              │
│  ┌────────────────────────────────────┐  │
│  │            PM2 CLI                │  │
│  │  (node로 실행됨)                  │  │
│  └──────────────┬────────────────────┘  │
│                 │ IPC 통신 (Unix Socket)│
│  ┌──────────────▼────────────────────┐  │
│  │         PM2 Daemon(Node 앱)       │  │
│  ├──────────────┬────────────────────┤  │
│  │              │ fork()              │
│  └──────────────▼────────────────────┘  │
│          Node 앱들 (app.js 등)         │
└──────────────────────────────────────────┘

Node.js 런타임이 PM2를 실행
PM2가 다시 Node.js를 이용해 앱들을 실행

정리

| 항목                | 내용                                          |
| ----------------- | ------------------------------------------- |
| **PM2 실행 기반**     | Node.js 런타임 위에서 동작                          |
| **Node.js 제공 방식** | 시스템에 설치된 Node.js 실행 파일(`/usr/bin/node`)을 이용 |
| **실행 명령 구조**      | `#!/usr/bin/env node` 로 Node 런타임을 호출        |
| **결과**            | Node.js → PM2 → (다른 Node 앱) 형태로 계층 실행       |

'#개발 > node.js' 카테고리의 다른 글

npm의 git:// 프로토콜 오류  (0) 2025.11.09

+ Recent posts